summaryrefslogtreecommitdiff
path: root/includes
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2010-07-28 11:52:48 +0200
committerPierre Schmitz <pierre@archlinux.de>2010-07-28 11:52:48 +0200
commit222b01f5169f1c7e69762e0e8904c24f78f71882 (patch)
tree8e932e12546bb991357ec48eb1638d1770be7a35 /includes
parent00ab76a6b686e98a914afc1975812d2b1aaa7016 (diff)
update to MediaWiki 1.16.0
Diffstat (limited to 'includes')
-rw-r--r--includes/AjaxDispatcher.php35
-rw-r--r--includes/AjaxFunctions.php96
-rw-r--r--includes/AjaxResponse.php44
-rw-r--r--includes/Article.php2217
-rw-r--r--includes/AuthPlugin.php47
-rw-r--r--includes/AutoLoader.php217
-rw-r--r--includes/Autopromote.php7
-rw-r--r--includes/BacklinkCache.php73
-rw-r--r--includes/BagOStuff.php660
-rw-r--r--includes/Block.php185
-rw-r--r--includes/CacheDependency.php43
-rw-r--r--includes/Category.php80
-rw-r--r--includes/CategoryPage.php233
-rw-r--r--includes/Categoryfinder.php142
-rw-r--r--includes/Cdb.php149
-rw-r--r--includes/Cdb_PHP.php374
-rw-r--r--includes/ChangeTags.php98
-rw-r--r--includes/ChangesFeed.php68
-rw-r--r--includes/ChangesList.php519
-rw-r--r--includes/ConfEditor.php1058
-rw-r--r--includes/Credits.php70
-rw-r--r--includes/DatabaseFunctions.php2
-rw-r--r--includes/DefaultSettings.php850
-rw-r--r--includes/Defines.php5
-rw-r--r--includes/DjVuImage.php26
-rw-r--r--includes/DoubleRedirectJob.php12
-rw-r--r--includes/EditPage.php1575
-rw-r--r--includes/Exception.php18
-rw-r--r--includes/Exif.php18
-rw-r--r--includes/Export.php73
-rw-r--r--includes/ExternalStore.php48
-rw-r--r--includes/ExternalStoreDB.php49
-rw-r--r--includes/ExternalStoreHttp.php18
-rw-r--r--includes/ExternalUser.php304
-rw-r--r--includes/FakeTitle.php83
-rw-r--r--includes/Feed.php111
-rw-r--r--includes/FeedUtils.php76
-rw-r--r--includes/FileDeleteForm.php60
-rw-r--r--includes/FileRevertForm.php13
-rw-r--r--includes/FileStore.php360
-rw-r--r--includes/ForkController.php24
-rw-r--r--includes/GlobalFunctions.php847
-rw-r--r--includes/HTMLCacheUpdate.php143
-rw-r--r--includes/HTMLFileCache.php21
-rw-r--r--includes/HTMLForm.php1391
-rw-r--r--includes/HistoryPage.php730
-rw-r--r--includes/Hooks.php80
-rw-r--r--includes/Html.php539
-rw-r--r--includes/HttpFunctions.php929
-rw-r--r--includes/IP.php160
-rw-r--r--includes/ImageFunctions.php6
-rw-r--r--includes/ImageGallery.php37
-rw-r--r--includes/ImagePage.php361
-rw-r--r--includes/ImageQueryPage.php18
-rw-r--r--includes/Import.php63
-rw-r--r--includes/Interwiki.php133
-rw-r--r--includes/JSMin.php290
-rw-r--r--includes/JobQueue.php42
-rw-r--r--includes/Licenses.php79
-rw-r--r--includes/LinkCache.php22
-rw-r--r--includes/LinkFilter.php84
-rw-r--r--includes/Linker.php1135
-rw-r--r--includes/LinksUpdate.php7
-rw-r--r--includes/LocalisationCache.php999
-rw-r--r--includes/LogEventsList.php628
-rw-r--r--includes/LogPage.php115
-rw-r--r--includes/MagicWord.php91
-rw-r--r--includes/Math.php39
-rw-r--r--includes/MediaTransformOutput.php30
-rw-r--r--includes/MessageCache.php295
-rw-r--r--includes/MimeMagic.php40
-rw-r--r--includes/Namespace.php47
-rw-r--r--includes/ObjectCache.php13
-rw-r--r--includes/OutputHandler.php22
-rw-r--r--includes/OutputPage.php1738
-rw-r--r--includes/PageHistory.php630
-rw-r--r--includes/Pager.php213
-rw-r--r--includes/PatrolLog.php36
-rw-r--r--includes/PoolCounter.php64
-rw-r--r--includes/Preferences.php1389
-rw-r--r--includes/PrefixSearch.php53
-rw-r--r--includes/Profiler.php15
-rw-r--r--includes/ProfilerSimpleText.php4
-rw-r--r--includes/ProtectionForm.php104
-rw-r--r--includes/ProxyTools.php29
-rw-r--r--includes/QueryPage.php174
-rw-r--r--includes/RawPage.php35
-rw-r--r--includes/RecentChange.php273
-rw-r--r--includes/RefreshLinksJob.php3
-rw-r--r--includes/Revision.php356
-rw-r--r--includes/Sanitizer.php392
-rw-r--r--includes/SearchMySQL.php270
-rw-r--r--includes/Setup.php100
-rw-r--r--includes/SiteConfiguration.php17
-rw-r--r--includes/SiteStats.php168
-rw-r--r--includes/Skin.php1064
-rw-r--r--includes/SkinTemplate.php593
-rw-r--r--includes/SpecialPage.php104
-rw-r--r--includes/SquidPurgeClient.php380
-rw-r--r--includes/SquidUpdate.php137
-rw-r--r--includes/Status.php16
-rw-r--r--includes/StreamFile.php7
-rw-r--r--includes/StubObject.php16
-rw-r--r--includes/Title.php740
-rw-r--r--includes/User.php829
-rw-r--r--includes/UserMailer.php19
-rw-r--r--includes/UserRightsProxy.php73
-rw-r--r--includes/WatchedItem.php4
-rw-r--r--includes/WatchlistEditor.php37
-rw-r--r--includes/WebRequest.php190
-rw-r--r--includes/WebResponse.php30
-rw-r--r--includes/WebStart.php7
-rw-r--r--includes/Wiki.php96
-rw-r--r--includes/WikiMap.php161
-rw-r--r--includes/Xml.php75
-rw-r--r--includes/ZhConversion.php1781
-rw-r--r--includes/api/ApiBase.php955
-rw-r--r--includes/api/ApiBlock.php105
-rw-r--r--includes/api/ApiDelete.php203
-rw-r--r--includes/api/ApiDisabled.php23
-rw-r--r--includes/api/ApiEditPage.php385
-rw-r--r--includes/api/ApiEmailUser.php34
-rw-r--r--includes/api/ApiExpandTemplates.php17
-rw-r--r--includes/api/ApiFeedWatchlist.php80
-rw-r--r--includes/api/ApiFormatBase.php115
-rw-r--r--includes/api/ApiFormatDbg.php18
-rw-r--r--includes/api/ApiFormatJson.php44
-rw-r--r--includes/api/ApiFormatPhp.php12
-rw-r--r--includes/api/ApiFormatRaw.php28
-rw-r--r--includes/api/ApiFormatTxt.php18
-rw-r--r--includes/api/ApiFormatWddx.php77
-rw-r--r--includes/api/ApiFormatXml.php117
-rw-r--r--includes/api/ApiFormatYaml.php12
-rw-r--r--includes/api/ApiFormatYaml_spyc.php98
-rw-r--r--includes/api/ApiHelp.php12
-rw-r--r--includes/api/ApiImport.php98
-rw-r--r--includes/api/ApiLogin.php99
-rw-r--r--includes/api/ApiLogout.php12
-rw-r--r--includes/api/ApiMain.php347
-rw-r--r--includes/api/ApiMove.php159
-rw-r--r--includes/api/ApiOpenSearch.php38
-rw-r--r--includes/api/ApiPageSet.php257
-rw-r--r--includes/api/ApiParamInfo.php134
-rw-r--r--includes/api/ApiParse.php194
-rw-r--r--includes/api/ApiPatrol.php50
-rw-r--r--includes/api/ApiProtect.php146
-rw-r--r--includes/api/ApiPurge.php41
-rw-r--r--includes/api/ApiQuery.php243
-rw-r--r--includes/api/ApiQueryAllCategories.php92
-rw-r--r--includes/api/ApiQueryAllLinks.php135
-rw-r--r--includes/api/ApiQueryAllUsers.php148
-rw-r--r--includes/api/ApiQueryAllimages.php121
-rw-r--r--includes/api/ApiQueryAllmessages.php118
-rw-r--r--includes/api/ApiQueryAllpages.php162
-rw-r--r--includes/api/ApiQueryBacklinks.php338
-rw-r--r--includes/api/ApiQueryBase.php196
-rw-r--r--includes/api/ApiQueryBlocks.php205
-rw-r--r--includes/api/ApiQueryCategories.php179
-rw-r--r--includes/api/ApiQueryCategoryInfo.php66
-rw-r--r--includes/api/ApiQueryCategoryMembers.php186
-rw-r--r--includes/api/ApiQueryDeletedrevs.php257
-rw-r--r--includes/api/ApiQueryDisabled.php12
-rw-r--r--includes/api/ApiQueryDuplicateFiles.php97
-rw-r--r--includes/api/ApiQueryExtLinksUsage.php125
-rw-r--r--includes/api/ApiQueryExternalLinks.php50
-rw-r--r--includes/api/ApiQueryImageInfo.php229
-rw-r--r--includes/api/ApiQueryImages.php101
-rw-r--r--includes/api/ApiQueryInfo.php447
-rw-r--r--includes/api/ApiQueryLangLinks.php75
-rw-r--r--includes/api/ApiQueryLinks.php120
-rw-r--r--includes/api/ApiQueryLogEvents.php274
-rw-r--r--includes/api/ApiQueryProtectedTitles.php112
-rw-r--r--includes/api/ApiQueryRandom.php82
-rw-r--r--includes/api/ApiQueryRecentChanges.php383
-rw-r--r--includes/api/ApiQueryRevisions.php435
-rw-r--r--includes/api/ApiQuerySearch.php129
-rw-r--r--includes/api/ApiQuerySiteinfo.php198
-rw-r--r--includes/api/ApiQueryTags.php181
-rw-r--r--includes/api/ApiQueryUserContributions.php305
-rw-r--r--includes/api/ApiQueryUserInfo.php99
-rw-r--r--includes/api/ApiQueryUsers.php202
-rw-r--r--includes/api/ApiQueryWatchlist.php316
-rw-r--r--includes/api/ApiQueryWatchlistRaw.php118
-rw-r--r--includes/api/ApiResult.php168
-rw-r--r--includes/api/ApiRollback.php76
-rw-r--r--includes/api/ApiUnblock.php59
-rw-r--r--includes/api/ApiUndelete.php87
-rw-r--r--includes/api/ApiUpload.php325
-rw-r--r--includes/api/ApiUserrights.php128
-rw-r--r--includes/api/ApiWatch.php46
-rw-r--r--includes/cbt/CBTCompiler.php366
-rw-r--r--includes/cbt/CBTProcessor.php539
-rw-r--r--includes/cbt/README108
-rw-r--r--includes/db/Database.php812
-rw-r--r--includes/db/DatabaseIbm_db2.php908
-rw-r--r--includes/db/DatabaseMssql.php86
-rw-r--r--includes/db/DatabaseMysql.php453
-rw-r--r--includes/db/DatabaseOracle.php1049
-rw-r--r--includes/db/DatabasePostgres.php184
-rw-r--r--includes/db/DatabaseSqlite.php432
-rw-r--r--includes/db/LBFactory.php14
-rw-r--r--includes/db/LoadBalancer.php21
-rw-r--r--includes/diff/DifferenceEngine.php933
-rw-r--r--includes/diff/DifferenceInterface.php1021
-rw-r--r--includes/diff/HTMLDiff.php1009
-rw-r--r--includes/diff/Nodes.php439
-rw-r--r--includes/extauth/Hardcoded.php79
-rw-r--r--includes/extauth/MediaWiki.php141
-rw-r--r--includes/extauth/vB.php140
-rw-r--r--includes/filerepo/ArchivedFile.php68
-rw-r--r--includes/filerepo/FSRepo.php103
-rw-r--r--includes/filerepo/File.php76
-rw-r--r--includes/filerepo/FileCache.php156
-rw-r--r--includes/filerepo/FileRepo.php175
-rw-r--r--includes/filerepo/ForeignAPIFile.php11
-rw-r--r--includes/filerepo/ForeignAPIRepo.php114
-rw-r--r--includes/filerepo/ForeignDBFile.php10
-rw-r--r--includes/filerepo/ForeignDBRepo.php15
-rw-r--r--includes/filerepo/ForeignDBViaLBRepo.php15
-rw-r--r--includes/filerepo/Image.php4
-rw-r--r--includes/filerepo/LocalFile.php224
-rw-r--r--includes/filerepo/LocalRepo.php97
-rw-r--r--includes/filerepo/NullRepo.php8
-rw-r--r--includes/filerepo/OldLocalFile.php24
-rw-r--r--includes/filerepo/RepoGroup.php124
-rw-r--r--includes/json/FormatJson.php32
-rw-r--r--includes/json/Services_JSON.php (renamed from includes/api/ApiFormatJson_json.php)20
-rw-r--r--includes/media/Bitmap.php121
-rw-r--r--includes/media/DjVu.php48
-rw-r--r--includes/media/GIF.php72
-rw-r--r--includes/media/GIFMetadataExtractor.php175
-rw-r--r--includes/media/Generic.php44
-rw-r--r--includes/media/SVG.php2
-rw-r--r--includes/memcached-client.php1990
-rw-r--r--includes/mime.types6
-rw-r--r--includes/normal/RandomTest.php2
-rw-r--r--includes/normal/Utf8CaseGenerate.php2
-rw-r--r--includes/normal/Utf8Test.php2
-rw-r--r--includes/normal/UtfNormal.php8
-rw-r--r--includes/normal/UtfNormalData.inc10
-rw-r--r--includes/normal/UtfNormalDataK.inc4
-rw-r--r--includes/normal/UtfNormalGenerate.php8
-rw-r--r--includes/parser/CoreParserFunctions.php148
-rw-r--r--includes/parser/CoreTagHooks.php49
-rw-r--r--includes/parser/DateFormatter.php10
-rw-r--r--includes/parser/LinkHolderArray.php3
-rw-r--r--includes/parser/Parser.php1121
-rw-r--r--includes/parser/ParserCache.php75
-rw-r--r--includes/parser/ParserOptions.php27
-rw-r--r--includes/parser/ParserOutput.php31
-rw-r--r--includes/parser/Preprocessor.php15
-rw-r--r--includes/parser/Preprocessor_DOM.php31
-rw-r--r--includes/parser/Preprocessor_Hash.php18
-rw-r--r--includes/search/SearchEngine.php (renamed from includes/SearchEngine.php)325
-rw-r--r--includes/search/SearchIBM_DB2.php (renamed from includes/SearchIBM_DB2.php)89
-rw-r--r--includes/search/SearchMySQL.php412
-rw-r--r--includes/search/SearchMySQL4.php (renamed from includes/SearchMySQL4.php)0
-rw-r--r--includes/search/SearchOracle.php (renamed from includes/SearchOracle.php)136
-rw-r--r--includes/search/SearchPostgres.php (renamed from includes/SearchPostgres.php)21
-rw-r--r--includes/search/SearchSqlite.php344
-rw-r--r--includes/search/SearchUpdate.php (renamed from includes/SearchUpdate.php)6
-rw-r--r--includes/specials/SpecialActiveusers.php195
-rw-r--r--includes/specials/SpecialAllmessages.php581
-rw-r--r--includes/specials/SpecialAllpages.php180
-rw-r--r--includes/specials/SpecialAncientpages.php28
-rw-r--r--includes/specials/SpecialBlankpage.php19
-rw-r--r--includes/specials/SpecialBlockip.php433
-rw-r--r--includes/specials/SpecialBooksources.php4
-rw-r--r--includes/specials/SpecialBrokenRedirects.php42
-rw-r--r--includes/specials/SpecialCategories.php13
-rw-r--r--includes/specials/SpecialConfirmemail.php14
-rw-r--r--includes/specials/SpecialContributions.php318
-rw-r--r--includes/specials/SpecialDeletedContributions.php243
-rw-r--r--includes/specials/SpecialDisambiguations.php2
-rw-r--r--includes/specials/SpecialDoubleRedirects.php28
-rw-r--r--includes/specials/SpecialEmailuser.php23
-rw-r--r--includes/specials/SpecialExport.php69
-rw-r--r--includes/specials/SpecialFewestrevisions.php21
-rw-r--r--includes/specials/SpecialFileDuplicateSearch.php23
-rw-r--r--includes/specials/SpecialFilepath.php4
-rw-r--r--includes/specials/SpecialImport.php15
-rw-r--r--includes/specials/SpecialIpblocklist.php64
-rw-r--r--includes/specials/SpecialLinkSearch.php18
-rw-r--r--includes/specials/SpecialListUserRestrictions.php162
-rw-r--r--includes/specials/SpecialListfiles.php22
-rw-r--r--includes/specials/SpecialListgrouprights.php74
-rw-r--r--includes/specials/SpecialListredirects.php9
-rw-r--r--includes/specials/SpecialListusers.php34
-rw-r--r--includes/specials/SpecialLockdb.php6
-rw-r--r--includes/specials/SpecialLog.php12
-rw-r--r--includes/specials/SpecialMIMEsearch.php20
-rw-r--r--includes/specials/SpecialMergeHistory.php38
-rw-r--r--includes/specials/SpecialMostlinked.php37
-rw-r--r--includes/specials/SpecialMostlinkedcategories.php2
-rw-r--r--includes/specials/SpecialMostlinkedtemplates.php33
-rw-r--r--includes/specials/SpecialMostrevisions.php9
-rw-r--r--includes/specials/SpecialMovepage.php138
-rw-r--r--includes/specials/SpecialNewimages.php81
-rw-r--r--includes/specials/SpecialNewpages.php38
-rw-r--r--includes/specials/SpecialPopularpages.php12
-rw-r--r--includes/specials/SpecialPreferences.php1315
-rw-r--r--includes/specials/SpecialPrefixindex.php28
-rw-r--r--includes/specials/SpecialProtectedpages.php47
-rw-r--r--includes/specials/SpecialProtectedtitles.php6
-rw-r--r--includes/specials/SpecialRandompage.php61
-rw-r--r--includes/specials/SpecialRandomredirect.php5
-rw-r--r--includes/specials/SpecialRecentchanges.php112
-rw-r--r--includes/specials/SpecialRecentchangeslinked.php72
-rw-r--r--includes/specials/SpecialRemoveRestrictions.php10
-rw-r--r--includes/specials/SpecialResetpass.php76
-rw-r--r--includes/specials/SpecialRestrictUser.php190
-rw-r--r--includes/specials/SpecialRevisiondelete.php2801
-rw-r--r--includes/specials/SpecialSearch.php1417
-rw-r--r--includes/specials/SpecialShortpages.php11
-rw-r--r--includes/specials/SpecialSpecialpages.php6
-rw-r--r--includes/specials/SpecialStatistics.php70
-rw-r--r--includes/specials/SpecialTags.php12
-rw-r--r--includes/specials/SpecialUncategorizedtemplates.php2
-rw-r--r--includes/specials/SpecialUndelete.php366
-rw-r--r--includes/specials/SpecialUnlockdb.php6
-rw-r--r--includes/specials/SpecialUnusedcategories.php2
-rw-r--r--includes/specials/SpecialUnusedimages.php16
-rw-r--r--includes/specials/SpecialUnusedtemplates.php13
-rw-r--r--includes/specials/SpecialUnwatchedpages.php12
-rw-r--r--includes/specials/SpecialUpload.php2374
-rw-r--r--includes/specials/SpecialUploadMogile.php135
-rw-r--r--includes/specials/SpecialUserlogin.php215
-rw-r--r--includes/specials/SpecialUserlogout.php10
-rw-r--r--includes/specials/SpecialUserrights.php390
-rw-r--r--includes/specials/SpecialVersion.php312
-rw-r--r--includes/specials/SpecialWantedcategories.php39
-rw-r--r--includes/specials/SpecialWantedfiles.php59
-rw-r--r--includes/specials/SpecialWantedpages.php93
-rw-r--r--includes/specials/SpecialWantedtemplates.php59
-rw-r--r--includes/specials/SpecialWatchlist.php120
-rw-r--r--includes/specials/SpecialWhatlinkshere.php84
-rw-r--r--includes/specials/SpecialWithoutinterwiki.php11
-rw-r--r--includes/templates/NoLocalSettings.php4
-rw-r--r--includes/templates/PHP4.php4
-rw-r--r--includes/templates/Userlogin.php111
-rw-r--r--includes/upload/UploadBase.php1091
-rw-r--r--includes/upload/UploadFromFile.php32
-rw-r--r--includes/upload/UploadFromStash.php84
-rw-r--r--includes/upload/UploadFromUrl.php137
-rw-r--r--includes/zhtable/Makefile2
-rw-r--r--includes/zhtable/Makefile.py83
-rw-r--r--includes/zhtable/simp2trad.manual459
-rw-r--r--includes/zhtable/simpphrases.manual122
-rw-r--r--includes/zhtable/simpphrases_exclude.manual4
-rw-r--r--includes/zhtable/toCN.manual12
-rw-r--r--includes/zhtable/toHK.manual206
-rw-r--r--includes/zhtable/toSimp.manual25
-rw-r--r--includes/zhtable/toTW.manual59
-rw-r--r--includes/zhtable/toTrad.manual77
-rw-r--r--includes/zhtable/trad2simp.manual265
-rw-r--r--includes/zhtable/tradphrases.manual972
-rw-r--r--includes/zhtable/tradphrases_exclude.manual53
357 files changed, 47259 insertions, 28880 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php
index c489cf1c..5bd7cfa4 100644
--- a/includes/AjaxDispatcher.php
+++ b/includes/AjaxDispatcher.php
@@ -7,7 +7,7 @@
* Handle ajax requests and send them to the proper handler.
*/
-if( !(defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) {
+if ( !( defined( 'MEDIAWIKI' ) && $wgUseAjax ) ) {
die( 1 );
}
@@ -33,11 +33,11 @@ class AjaxDispatcher {
$this->mode = "";
- if (! empty($_GET["rs"])) {
+ if ( ! empty( $_GET["rs"] ) ) {
$this->mode = "get";
}
- if (!empty($_POST["rs"])) {
+ if ( !empty( $_POST["rs"] ) ) {
$this->mode = "post";
}
@@ -45,7 +45,7 @@ class AjaxDispatcher {
case 'get':
$this->func_name = isset( $_GET["rs"] ) ? $_GET["rs"] : '';
- if (! empty($_GET["rsargs"])) {
+ if ( ! empty( $_GET["rsargs"] ) ) {
$this->args = $_GET["rsargs"];
} else {
$this->args = array();
@@ -54,7 +54,7 @@ class AjaxDispatcher {
case 'post':
$this->func_name = isset( $_POST["rs"] ) ? $_POST["rs"] : '';
- if (! empty($_POST["rsargs"])) {
+ if ( ! empty( $_POST["rsargs"] ) ) {
$this->args = $_POST["rsargs"];
} else {
$this->args = array();
@@ -65,7 +65,7 @@ class AjaxDispatcher {
wfProfileOut( __METHOD__ );
return;
# Or we could throw an exception:
- #throw new MWException( __METHOD__ . ' called without any data (mode empty).' );
+ # throw new MWException( __METHOD__ . ' called without any data (mode empty).' );
}
@@ -83,9 +83,10 @@ class AjaxDispatcher {
if ( empty( $this->mode ) ) {
return;
}
+
wfProfileIn( __METHOD__ );
- if (! in_array( $this->func_name, $wgAjaxExportList ) ) {
+ if ( ! in_array( $this->func_name, $wgAjaxExportList ) ) {
wfDebug( __METHOD__ . ' Bad Request for unknown function ' . $this->func_name . "\n" );
wfHttpError( 400, 'Bad Request',
@@ -99,11 +100,11 @@ class AjaxDispatcher {
$func = $this->func_name;
}
try {
- $result = call_user_func_array($func, $this->args);
+ $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 ) . "): "
+ 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',
@@ -111,7 +112,7 @@ class AjaxDispatcher {
}
else {
if ( is_string( $result ) ) {
- $result= new AjaxResponse( $result );
+ $result = new AjaxResponse( $result );
}
$result->sendHeaders();
@@ -120,12 +121,12 @@ class AjaxDispatcher {
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" );
+ } 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()) {
+ if ( !headers_sent() ) {
wfHttpError( 500, 'Internal Error',
$e->getMessage() );
} else {
diff --git a/includes/AjaxFunctions.php b/includes/AjaxFunctions.php
index 1a9adbca..e3180e0a 100644
--- a/includes/AjaxFunctions.php
+++ b/includes/AjaxFunctions.php
@@ -4,7 +4,7 @@
* @ingroup Ajax
*/
-if( !defined( 'MEDIAWIKI' ) ) {
+if ( !defined( 'MEDIAWIKI' ) ) {
die( 1 );
}
@@ -14,31 +14,31 @@ if( !defined( 'MEDIAWIKI' ) ) {
* Modified function from http://pure-essence.net/stuff/code/utf8RawUrlDecode.phps
*
* @param $source String escaped with Javascript's escape() function
- * @param $iconv_to String destination character set will be used as second parameter
+ * @param $iconv_to String destination character set will be used as second parameter
* in the iconv function. Default is UTF-8.
* @return string
*/
-function js_unescape($source, $iconv_to = 'UTF-8') {
+function js_unescape( $source, $iconv_to = 'UTF-8' ) {
$decodedStr = '';
$pos = 0;
- $len = strlen ($source);
+ $len = strlen ( $source );
- while ($pos < $len) {
- $charAt = substr ($source, $pos, 1);
- if ($charAt == '%') {
+ while ( $pos < $len ) {
+ $charAt = substr ( $source, $pos, 1 );
+ if ( $charAt == '%' ) {
$pos++;
- $charAt = substr ($source, $pos, 1);
- if ($charAt == 'u') {
+ $charAt = substr ( $source, $pos, 1 );
+ if ( $charAt == 'u' ) {
// we got a unicode character
$pos++;
- $unicodeHexVal = substr ($source, $pos, 4);
- $unicode = hexdec ($unicodeHexVal);
- $decodedStr .= code2utf($unicode);
+ $unicodeHexVal = substr ( $source, $pos, 4 );
+ $unicode = hexdec ( $unicodeHexVal );
+ $decodedStr .= code2utf( $unicode );
$pos += 4;
} else {
// we have an escaped ascii character
- $hexVal = substr ($source, $pos, 2);
- $decodedStr .= chr (hexdec ($hexVal));
+ $hexVal = substr ( $source, $pos, 2 );
+ $decodedStr .= chr ( hexdec ( $hexVal ) );
$pos += 2;
}
} else {
@@ -47,8 +47,8 @@ function js_unescape($source, $iconv_to = 'UTF-8') {
}
}
- if ($iconv_to != "UTF-8") {
- $decodedStr = iconv("UTF-8", $iconv_to, $decodedStr);
+ if ( $iconv_to != "UTF-8" ) {
+ $decodedStr = iconv( "UTF-8", $iconv_to, $decodedStr );
}
return $decodedStr;
@@ -61,16 +61,16 @@ function js_unescape($source, $iconv_to = 'UTF-8') {
* @param $num Integer
* @return utf8char
*/
-function code2utf($num){
- if ( $num<128 )
- return chr($num);
- if ( $num<2048 )
- return chr(($num>>6)+192).chr(($num&63)+128);
- if ( $num<65536 )
- return chr(($num>>12)+224).chr((($num>>6)&63)+128).chr(($num&63)+128);
- if ( $num<2097152 )
- return chr(($num>>18)+240).chr((($num>>12)&63)+128).chr((($num>>6)&63)+128) .chr(($num&63)+128);
- return '';
+function code2utf( $num ) {
+ if ( $num < 128 )
+ return chr( $num );
+ if ( $num < 2048 )
+ return chr( ( $num >> 6 ) + 192 ) . chr( ( $num&63 ) + 128 );
+ if ( $num < 65536 )
+ return chr( ( $num >> 12 ) + 224 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 );
+ if ( $num < 2097152 )
+ return chr( ( $num >> 18 ) + 240 ) . chr( ( ( $num >> 12 )&63 ) + 128 ) . chr( ( ( $num >> 6 )&63 ) + 128 ) . chr( ( $num&63 ) + 128 );
+ return '';
}
/**
@@ -81,49 +81,49 @@ function code2utf($num){
* respectively, followed by an HTML message to display in the alert box; or
* '<err#>' on error
*/
-function wfAjaxWatch($pagename = "", $watch = "") {
- if(wfReadOnly()) {
+function wfAjaxWatch( $pagename = "", $watch = "" ) {
+ if ( wfReadOnly() ) {
// redirect to action=(un)watch, which will display the database lock
// message
return '<err#>';
}
- if('w' !== $watch && 'u' !== $watch) {
+ if ( 'w' !== $watch && 'u' !== $watch ) {
return '<err#>';
}
$watch = 'w' === $watch;
- $title = Title::newFromDBkey($pagename);
- if(!$title) {
+ $title = Title::newFromDBkey( $pagename );
+ if ( !$title ) {
// Invalid title
return '<err#>';
}
- $article = new Article($title);
+ $article = new Article( $title );
$watching = $title->userIsWatching();
- if($watch) {
- if(!$watching) {
- $dbw = wfGetDB(DB_MASTER);
+ if ( $watch ) {
+ if ( !$watching ) {
+ $dbw = wfGetDB( DB_MASTER );
$dbw->begin();
$ok = $article->doWatch();
$dbw->commit();
}
} else {
- if($watching) {
- $dbw = wfGetDB(DB_MASTER);
+ if ( $watching ) {
+ $dbw = wfGetDB( DB_MASTER );
$dbw->begin();
$ok = $article->doUnwatch();
$dbw->commit();
}
}
// Something stopped the change
- if( isset($ok) && !$ok ) {
+ if ( isset( $ok ) && !$ok ) {
return '<err#>';
}
- if( $watch ) {
- return '<w#>'.wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() );
+ if ( $watch ) {
+ return '<w#>' . wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() );
} else {
- return '<u#>'.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() );
+ return '<u#>' . wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() );
}
}
@@ -133,12 +133,12 @@ function wfAjaxWatch($pagename = "", $watch = "") {
*/
function wfAjaxGetThumbnailUrl( $file, $width, $height ) {
$file = wfFindFile( $file );
-
+
if ( !$file || !$file->exists() )
return null;
-
+
$url = $file->getThumbnail( $width, $height )->url;
-
+
return $url;
}
@@ -148,11 +148,11 @@ function wfAjaxGetThumbnailUrl( $file, $width, $height ) {
*/
function wfAjaxGetFileUrl( $file ) {
$file = wfFindFile( $file );
-
+
if ( !$file || !$file->exists() )
return null;
-
+
$url = $file->getUrl();
-
+
return $url;
-} \ No newline at end of file
+}
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php
index 26b6f443..f7495666 100644
--- a/includes/AjaxResponse.php
+++ b/includes/AjaxResponse.php
@@ -4,14 +4,14 @@
* @ingroup Ajax
*/
-if( !defined( 'MEDIAWIKI' ) ) {
+if ( !defined( 'MEDIAWIKI' ) ) {
die( 1 );
}
/**
* Handle responses for Ajax requests (send headers, print
* content, that sort of thing)
- *
+ *
* @ingroup Ajax
*/
class AjaxResponse {
@@ -37,15 +37,15 @@ class AjaxResponse {
/** Content of our HTTP response */
private $mText;
- function __construct( $text = NULL ) {
- $this->mCacheDuration = NULL;
- $this->mVary = NULL;
+ function __construct( $text = null ) {
+ $this->mCacheDuration = null;
+ $this->mVary = null;
$this->mDisabled = false;
$this->mText = '';
$this->mResponseCode = '200 OK';
$this->mLastModified = false;
- $this->mContentType= 'application/x-wiki';
+ $this->mContentType = 'application/x-wiki';
if ( $text ) {
$this->addText( $text );
@@ -95,13 +95,13 @@ class AjaxResponse {
header( "Status: " . $this->mResponseCode, true, (int)$n );
}
- header ("Content-Type: " . $this->mContentType );
+ header ( "Content-Type: " . $this->mContentType );
if ( $this->mLastModified ) {
- header ("Last-Modified: " . $this->mLastModified );
+ header ( "Last-Modified: " . $this->mLastModified );
}
else {
- header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
+ header ( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
}
if ( $this->mCacheDuration ) {
@@ -110,31 +110,31 @@ class AjaxResponse {
# and tell the client to always check with the squid. Otherwise,
# tell the client to use a cached copy, without a way to purge it.
- if( $wgUseSquid ) {
+ if ( $wgUseSquid ) {
# Expect explicite purge of the proxy cache, but require end user agents
# to revalidate against the proxy on each visit.
# Surrogate-Control controls our Squid, Cache-Control downstream caches
if ( $wgUseESI ) {
- header( 'Surrogate-Control: max-age='.$this->mCacheDuration.', content="ESI/1.0"');
+ header( 'Surrogate-Control: max-age=' . $this->mCacheDuration . ', content="ESI/1.0"' );
header( 'Cache-Control: s-maxage=0, must-revalidate, max-age=0' );
} else {
- header( 'Cache-Control: s-maxage='.$this->mCacheDuration.', must-revalidate, max-age=0' );
+ header( 'Cache-Control: s-maxage=' . $this->mCacheDuration . ', must-revalidate, max-age=0' );
}
} else {
# Let the client do the caching. Cache is not purged.
- header ("Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT");
- header ("Cache-Control: s-max-age={$this->mCacheDuration},public,max-age={$this->mCacheDuration}");
+ header ( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" );
+ header ( "Cache-Control: s-max-age={$this->mCacheDuration},public,max-age={$this->mCacheDuration}" );
}
} else {
# always expired, always modified
- header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
- header ("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
- header ("Pragma: no-cache"); // HTTP/1.0
+ header ( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past
+ header ( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1
+ header ( "Pragma: no-cache" ); // HTTP/1.0
}
if ( $this->mVary ) {
@@ -156,11 +156,11 @@ class AjaxResponse {
wfDebug( "$fname: CACHE DISABLED, NO TIMESTAMP\n" );
return;
}
- if( !$wgCachePages ) {
+ if ( !$wgCachePages ) {
wfDebug( "$fname: CACHE DISABLED\n", false );
return;
}
- if( $wgUser->getOption( 'nocache' ) ) {
+ if ( $wgUser->getOption( 'nocache' ) ) {
wfDebug( "$fname: USER DISABLED CACHE\n", false );
return;
}
@@ -168,7 +168,7 @@ class AjaxResponse {
$timestamp = wfTimestamp( TS_MW, $timestamp );
$lastmod = wfTimestamp( TS_RFC2822, max( $timestamp, $wgUser->mTouched, $wgCacheEpoch ) );
- if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
+ if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
# IE sends sizes after the date like this:
# Wed, 20 Aug 2003 06:51:19 GMT; length=5202
# this breaks strtotime().
@@ -177,8 +177,8 @@ class AjaxResponse {
$ismodsince = wfTimestamp( TS_MW, $modsinceTime ? $modsinceTime : 1 );
wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false );
wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false );
- if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) {
- ini_set('zlib.output_compression', 0);
+ if ( ( $ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) {
+ ini_set( 'zlib.output_compression', 0 );
$this->setResponseCode( "304 Not Modified" );
$this->disable();
$this->mLastModified = $lastmod;
diff --git a/includes/Article.php b/includes/Article.php
index ef219ea3..d3863c77 100644
--- a/includes/Article.php
+++ b/includes/Article.php
@@ -16,30 +16,32 @@ class Article {
/**@{{
* @private
*/
- var $mComment = ''; //!<
- var $mContent; //!<
- var $mContentLoaded = false; //!<
- var $mCounter = -1; //!< Not loaded
- var $mCurID = -1; //!< Not loaded
- var $mDataLoaded = false; //!<
- var $mForUpdate = false; //!<
- var $mGoodAdjustment = 0; //!<
- var $mIsRedirect = false; //!<
- var $mLatest = false; //!<
- var $mMinorEdit; //!<
- var $mOldId; //!<
- var $mPreparedEdit = false; //!< Title object if set
- var $mRedirectedFrom = null; //!< Title object if set
- var $mRedirectTarget = null; //!< Title object if set
- var $mRedirectUrl = false; //!<
- var $mRevIdFetched = 0; //!<
- var $mRevision; //!<
- var $mTimestamp = ''; //!<
- var $mTitle; //!<
- var $mTotalAdjustment = 0; //!<
- var $mTouched = '19700101000000'; //!<
- var $mUser = -1; //!< Not loaded
- var $mUserText = ''; //!<
+ var $mComment = ''; // !<
+ var $mContent; // !<
+ var $mContentLoaded = false; // !<
+ var $mCounter = -1; // !< Not loaded
+ var $mCurID = -1; // !< Not loaded
+ var $mDataLoaded = false; // !<
+ var $mForUpdate = false; // !<
+ var $mGoodAdjustment = 0; // !<
+ var $mIsRedirect = false; // !<
+ var $mLatest = false; // !<
+ var $mMinorEdit; // !<
+ var $mOldId; // !<
+ var $mPreparedEdit = false; // !< Title object if set
+ var $mRedirectedFrom = null; // !< Title object if set
+ var $mRedirectTarget = null; // !< Title object if set
+ var $mRedirectUrl = false; // !<
+ var $mRevIdFetched = 0; // !<
+ var $mRevision; // !<
+ var $mTimestamp = ''; // !<
+ var $mTitle; // !<
+ var $mTotalAdjustment = 0; // !<
+ var $mTouched = '19700101000000'; // !<
+ var $mUser = -1; // !< Not loaded
+ var $mUserText = ''; // !<
+ var $mParserOptions; // !<
+ var $mParserOutput; // !<
/**@}}*/
/**
@@ -58,7 +60,9 @@ class Article {
*/
public static function newFromID( $id ) {
$t = Title::newFromID( $id );
- return $t == null ? null : new Article( $t );
+ # FIXME: doesn't inherit right
+ return $t == null ? null : new self( $t );
+ # return $t == null ? null : new static( $t ); // PHP 5.3
}
/**
@@ -78,19 +82,19 @@ class Article {
* @return mixed Title object, or null if this page is not a redirect
*/
public function getRedirectTarget() {
- if( !$this->mTitle || !$this->mTitle->isRedirect() )
+ if ( !$this->mTitle || !$this->mTitle->isRedirect() )
return null;
- if( !is_null($this->mRedirectTarget) )
+ if ( !is_null( $this->mRedirectTarget ) )
return $this->mRedirectTarget;
# Query the redirect table
$dbr = wfGetDB( DB_SLAVE );
$row = $dbr->selectRow( 'redirect',
- array('rd_namespace', 'rd_title'),
- array('rd_from' => $this->getID() ),
+ array( 'rd_namespace', 'rd_title' ),
+ array( 'rd_from' => $this->getID() ),
__METHOD__
);
- if( $row ) {
- return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title);
+ 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();
@@ -104,15 +108,15 @@ class Article {
*/
public function insertRedirect() {
$retval = Title::newFromRedirect( $this->getContent() );
- if( !$retval ) {
+ if ( !$retval ) {
return null;
}
$dbw = wfGetDB( DB_MASTER );
- $dbw->replace( 'redirect', array('rd_from'),
+ $dbw->replace( 'redirect', array( 'rd_from' ),
array(
'rd_from' => $this->getID(),
'rd_namespace' => $retval->getNamespace(),
- 'rd_title' => $retval->getDBKey()
+ 'rd_title' => $retval->getDBkey()
),
__METHOD__
);
@@ -137,9 +141,9 @@ class Article {
public function followRedirectText( $text ) {
$rt = Title::newFromRedirectRecurse( $text ); // recurse through to only get the final target
# process if title object is valid and not special:userlogout
- if( $rt ) {
- if( $rt->getInterwiki() != '' ) {
- if( $rt->isLocal() ) {
+ if ( $rt ) {
+ if ( $rt->getInterwiki() != '' ) {
+ if ( $rt->isLocal() ) {
// Offsite wikis need an HTTP redirect.
//
// This can be hard to reverse and may produce loops,
@@ -148,13 +152,13 @@ class Article {
return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) );
}
} else {
- if( $rt->getNamespace() == NS_SPECIAL ) {
+ if ( $rt->getNamespace() == NS_SPECIAL ) {
// Gotta handle redirects to special pages differently:
// Fill the HTTP response "Location" header and ignore
// the rest of the page we're on.
//
// This can be hard to reverse, so they may be disabled.
- if( $rt->isSpecial( 'Userlogout' ) ) {
+ if ( $rt->isSpecial( 'Userlogout' ) ) {
// rolleyes
} else {
return $rt->getFullURL();
@@ -203,19 +207,19 @@ class Article {
* the shortcut in Article::followContent()
*
* @return Return the text of this revision
- */
+ */
public function getContent() {
global $wgUser, $wgContLang, $wgOut, $wgMessageCache;
wfProfileIn( __METHOD__ );
- if( $this->getID() === 0 ) {
+ if ( $this->getID() === 0 ) {
# If this is a MediaWiki:x message, then load the messages
# and return the message value for x.
- if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
# If this is a system message, get the default text.
list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) );
$wgMessageCache->loadAllMessages( $lang );
$text = wfMsgGetKey( $message, false, $lang, false );
- if( wfEmptyMsg( $message, $text ) )
+ if ( wfEmptyMsg( $message, $text ) )
$text = '';
} else {
$text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
@@ -228,15 +232,15 @@ class Article {
return $this->mContent;
}
}
-
+
/**
* Get the text of the current revision. No side-effects...
*
* @return Return the text of the current revision
- */
+ */
public function getRawText() {
// Check process cache for current revision
- if( $this->mContentLoaded && $this->mOldId == 0 ) {
+ if ( $this->mContentLoaded && $this->mOldId == 0 ) {
return $this->mContent;
}
$rev = Revision::newFromTitle( $this->mTitle );
@@ -260,12 +264,12 @@ class Article {
global $wgParser;
return $wgParser->getSection( $text, $section );
}
-
+
/**
* Get the text that needs to be saved in order to undo all revisions
* between $undo and $undoafter. Revisions must belong to the same page,
* must exist and must not be deleted
- * @param $undo Revision
+ * @param $undo Revision
* @param $undoafter Revision Must be an earlier revision than $undo
* @return mixed string on success, false on failure
*/
@@ -288,7 +292,7 @@ class Article {
* current revision
*/
public function getOldID() {
- if( is_null( $this->mOldId ) ) {
+ if ( is_null( $this->mOldId ) ) {
$this->mOldId = $this->getOldIDFromRequest();
}
return $this->mOldId;
@@ -303,23 +307,23 @@ class Article {
global $wgRequest;
$this->mRedirectUrl = false;
$oldid = $wgRequest->getVal( 'oldid' );
- if( isset( $oldid ) ) {
+ if ( isset( $oldid ) ) {
$oldid = intval( $oldid );
- if( $wgRequest->getVal( 'direction' ) == 'next' ) {
+ if ( $wgRequest->getVal( 'direction' ) == 'next' ) {
$nextid = $this->mTitle->getNextRevisionID( $oldid );
- if( $nextid ) {
+ if ( $nextid ) {
$oldid = $nextid;
} else {
$this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' );
}
- } elseif( $wgRequest->getVal( 'direction' ) == 'prev' ) {
+ } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) {
$previd = $this->mTitle->getPreviousRevisionID( $oldid );
- if( $previd ) {
+ if ( $previd ) {
$oldid = $previd;
}
}
}
- if( !$oldid ) {
+ if ( !$oldid ) {
$oldid = 0;
}
return $oldid;
@@ -329,7 +333,7 @@ class Article {
* Load the revision (including text) into this object
*/
function loadContent() {
- if( $this->mContentLoaded ) return;
+ if ( $this->mContentLoaded ) return;
wfProfileIn( __METHOD__ );
# Query variables :P
$oldid = $this->getOldID();
@@ -396,26 +400,26 @@ class Article {
* @param $data Database row object or "fromdb"
*/
public function loadPageData( $data = 'fromdb' ) {
- if( $data === 'fromdb' ) {
+ if ( $data === 'fromdb' ) {
$dbr = wfGetDB( DB_MASTER );
$data = $this->pageDataFromId( $dbr, $this->getId() );
}
$lc = LinkCache::singleton();
- if( $data ) {
+ if ( $data ) {
$lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect );
- $this->mTitle->mArticleID = $data->page_id;
+ $this->mTitle->mArticleID = intval( $data->page_id );
# Old-fashioned restrictions
$this->mTitle->loadRestrictions( $data->page_restrictions );
- $this->mCounter = $data->page_counter;
+ $this->mCounter = intval( $data->page_counter );
$this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
- $this->mIsRedirect = $data->page_is_redirect;
- $this->mLatest = $data->page_latest;
+ $this->mIsRedirect = intval( $data->page_is_redirect );
+ $this->mLatest = intval( $data->page_latest );
} else {
- if( is_object( $this->mTitle ) ) {
+ if ( is_object( $this->mTitle ) ) {
$lc->addBadLinkObj( $this->mTitle );
}
$this->mTitle->mArticleID = 0;
@@ -431,7 +435,7 @@ class Article {
* @return string
*/
function fetchContent( $oldid = 0 ) {
- if( $this->mContentLoaded ) {
+ if ( $this->mContentLoaded ) {
return $this->mContent;
}
@@ -441,33 +445,33 @@ class Article {
# fails we'll have something telling us what we intended.
$t = $this->mTitle->getPrefixedText();
$d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
- $this->mContent = wfMsg( 'missing-article', $t, $d ) ;
+ $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
- if( $oldid ) {
+ if ( $oldid ) {
$revision = Revision::newFromId( $oldid );
- if( is_null( $revision ) ) {
- wfDebug( __METHOD__." failed to retrieve specified revision, id $oldid\n" );
+ if ( is_null( $revision ) ) {
+ wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" );
return false;
}
$data = $this->pageDataFromId( $dbr, $revision->getPage() );
- if( !$data ) {
- wfDebug( __METHOD__." failed to get page data linked to revision id $oldid\n" );
+ if ( !$data ) {
+ wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" );
return false;
}
$this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title );
$this->loadPageData( $data );
} else {
- if( !$this->mDataLoaded ) {
+ if ( !$this->mDataLoaded ) {
$data = $this->pageDataFromTitle( $dbr, $this->mTitle );
- if( !$data ) {
- wfDebug( __METHOD__." failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" );
+ if ( !$data ) {
+ wfDebug( __METHOD__ . " failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" );
return false;
}
$this->loadPageData( $data );
}
$revision = Revision::newFromId( $this->mLatest );
- if( is_null( $revision ) ) {
- wfDebug( __METHOD__." failed to retrieve current page, rev_id {$this->mLatest}\n" );
+ if ( is_null( $revision ) ) {
+ wfDebug( __METHOD__ . " failed to retrieve current page, rev_id {$this->mLatest}\n" );
return false;
}
}
@@ -495,7 +499,7 @@ class Article {
*
* @param $x Mixed: FIXME
*/
- public function forUpdate( $x = NULL ) {
+ public function forUpdate( $x = null ) {
return wfSetVar( $this->mForUpdate, $x );
}
@@ -518,8 +522,8 @@ class Article {
* @return Array: options
*/
protected function getSelectOptions( $options = '' ) {
- if( $this->mForUpdate ) {
- if( is_array( $options ) ) {
+ if ( $this->mForUpdate ) {
+ if ( is_array( $options ) ) {
$options[] = 'FOR UPDATE';
} else {
$options = 'FOR UPDATE';
@@ -532,7 +536,7 @@ class Article {
* @return int Page ID
*/
public function getID() {
- if( $this->mTitle ) {
+ if ( $this->mTitle ) {
return $this->mTitle->getArticleID();
} else {
return 0;
@@ -545,7 +549,7 @@ class Article {
public function exists() {
return $this->getId() > 0;
}
-
+
/**
* Check if this page is something we're going to be showing
* some sort of sensible content for. If we return false, page
@@ -562,16 +566,16 @@ class Article {
* @return int The view count for the page
*/
public function getCount() {
- if( -1 == $this->mCounter ) {
+ if ( -1 == $this->mCounter ) {
$id = $this->getID();
- if( $id == 0 ) {
+ if ( $id == 0 ) {
$this->mCounter = 0;
} else {
$dbr = wfGetDB( DB_SLAVE );
- $this->mCounter = $dbr->selectField( 'page',
- 'page_counter',
- array( 'page_id' => $id ),
- __METHOD__,
+ $this->mCounter = $dbr->selectField( 'page',
+ 'page_counter',
+ array( 'page_id' => $id ),
+ __METHOD__,
$this->getSelectOptions()
);
}
@@ -590,7 +594,7 @@ class Article {
global $wgUseCommaCount;
$token = $wgUseCommaCount ? ',' : '[[';
- return $this->mTitle->isContentPage() && !$this->isRedirect($text) && in_string($token,$text);
+ return $this->mTitle->isContentPage() && !$this->isRedirect( $text ) && in_string( $token, $text );
}
/**
@@ -600,8 +604,8 @@ class Article {
* @return bool
*/
public function isRedirect( $text = false ) {
- if( $text === false ) {
- if( $this->mDataLoaded ) {
+ if ( $text === false ) {
+ if ( $this->mDataLoaded ) {
return $this->mIsRedirect;
}
// Apparently loadPageData was never called
@@ -610,7 +614,7 @@ class Article {
} else {
$titleObj = Title::newFromRedirect( $text );
}
- return $titleObj !== NULL;
+ return $titleObj !== null;
}
/**
@@ -620,10 +624,10 @@ class Article {
*/
public function isCurrent() {
# If no oldid, this is the current version.
- if( $this->getOldID() == 0 ) {
+ if ( $this->getOldID() == 0 ) {
return true;
}
- return $this->exists() && isset($this->mRevision) && $this->mRevision->isCurrent();
+ return $this->exists() && isset( $this->mRevision ) && $this->mRevision->isCurrent();
}
/**
@@ -631,15 +635,15 @@ class Article {
* This isn't necessary for all uses, so it's only done if needed.
*/
protected function loadLastEdit() {
- if( -1 != $this->mUser )
+ if ( -1 != $this->mUser )
return;
# New or non-existent articles have no user information
$id = $this->getID();
- if( 0 == $id ) return;
+ if ( 0 == $id ) return;
$this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id );
- if( !is_null( $this->mLastRevision ) ) {
+ if ( !is_null( $this->mLastRevision ) ) {
$this->mUser = $this->mLastRevision->getUser();
$this->mUserText = $this->mLastRevision->getUserText();
$this->mTimestamp = $this->mLastRevision->getTimestamp();
@@ -651,10 +655,10 @@ class Article {
public function getTimestamp() {
// Check if the field has been filled by ParserCache::get()
- if( !$this->mTimestamp ) {
+ if ( !$this->mTimestamp ) {
$this->loadLastEdit();
}
- return wfTimestamp(TS_MW, $this->mTimestamp);
+ return wfTimestamp( TS_MW, $this->mTimestamp );
}
public function getUser() {
@@ -687,69 +691,77 @@ class Article {
* @param $limit Integer: default 0.
* @param $offset Integer: default 0.
*/
- public function getContributors($limit = 0, $offset = 0) {
+ public function getContributors( $limit = 0, $offset = 0 ) {
# XXX: this is expensive; cache this info somewhere.
- $contribs = array();
$dbr = wfGetDB( DB_SLAVE );
$revTable = $dbr->tableName( 'revision' );
$userTable = $dbr->tableName( 'user' );
- $user = $this->getUser();
+
$pageId = $this->getId();
- $hideBit = Revision::DELETED_USER; // username hidden?
+ $user = $this->getUser();
+ if ( $user ) {
+ $excludeCond = "AND rev_user != $user";
+ } else {
+ $userText = $dbr->addQuotes( $this->getUserText() );
+ $excludeCond = "AND rev_user_text != $userText";
+ }
- $sql = "SELECT {$userTable}.*, MAX(rev_timestamp) as timestamp
+ $deletedBit = $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ); // username hidden?
+
+ $sql = "SELECT {$userTable}.*, rev_user_text as user_name, MAX(rev_timestamp) as timestamp
FROM $revTable LEFT JOIN $userTable ON rev_user = user_id
WHERE rev_page = $pageId
- AND rev_user != $user
- AND rev_deleted & $hideBit = 0
- GROUP BY rev_user, rev_user_text, user_real_name
+ $excludeCond
+ AND $deletedBit = 0
+ GROUP BY rev_user, rev_user_text
ORDER BY timestamp DESC";
- if($limit > 0) { $sql .= ' LIMIT '.$limit; }
- if($offset > 0) { $sql .= ' OFFSET '.$offset; }
-
- $sql .= ' '. $this->getSelectOptions();
+ if ( $limit > 0 )
+ $sql = $dbr->limitResult( $sql, $limit, $offset );
- $res = $dbr->query($sql, __METHOD__ );
+ $sql .= ' ' . $this->getSelectOptions();
+ $res = $dbr->query( $sql, __METHOD__ );
return new UserArrayFromResult( $res );
}
/**
- * This is the default action of the script: just view the page of
- * the given title.
- */
+ * This is the default action of the index.php entry point: just view the
+ * page of the given title.
+ */
public function view() {
global $wgUser, $wgOut, $wgRequest, $wgContLang;
global $wgEnableParserCache, $wgStylePath, $wgParser;
- global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies;
- global $wgDefaultRobotPolicy;
+ global $wgUseTrackbacks, $wgUseFileCache;
- # Let the parser know if this is the printable version
- if( $wgOut->isPrintable() ) {
- $wgOut->parserOptions()->setIsPrintable( true );
- }
-
wfProfileIn( __METHOD__ );
# Get variables from query string
$oldid = $this->getOldID();
+ $parserCache = ParserCache::singleton();
+
+ $parserOptions = clone $this->getParserOptions();
+ # Render printable version, use printable version cache
+ if ( $wgOut->isPrintable() ) {
+ $parserOptions->setIsPrintable( true );
+ }
# Try client and file cache
- if( $oldid === 0 && $this->checkTouched() ) {
+ if ( $oldid === 0 && $this->checkTouched() ) {
global $wgUseETag;
- if( $wgUseETag ) {
- $parserCache = ParserCache::singleton();
- $wgOut->setETag( $parserCache->getETag($this, $wgOut->parserOptions()) );
+ if ( $wgUseETag ) {
+ $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) );
}
# Is is client cached?
- if( $wgOut->checkLastModified( $this->getTouched() ) ) {
+ if ( $wgOut->checkLastModified( $this->getTouched() ) ) {
+ wfDebug( __METHOD__ . ": done 304\n" );
wfProfileOut( __METHOD__ );
return;
# Try file cache
- } else if( $this->tryFileCache() ) {
+ } else if ( $wgUseFileCache && $this->tryFileCache() ) {
+ wfDebug( __METHOD__ . ": done file cache\n" );
# tell wgOut that output is taken care of
$wgOut->disable();
$this->viewUpdates();
@@ -758,91 +770,355 @@ class Article {
}
}
- $ns = $this->mTitle->getNamespace(); # shortcut
$sk = $wgUser->getSkin();
# getOldID may want us to redirect somewhere else
- if( $this->mRedirectUrl ) {
+ if ( $this->mRedirectUrl ) {
$wgOut->redirect( $this->mRedirectUrl );
+ wfDebug( __METHOD__ . ": redirecting due to oldid\n" );
wfProfileOut( __METHOD__ );
return;
}
+ $wgOut->setArticleFlag( true );
+ # Set page title (may be overridden by DISPLAYTITLE)
+ $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+
+ # If we got diff in the query, we want to see a diff page instead of the article.
+ if ( !is_null( $wgRequest->getVal( 'diff' ) ) ) {
+ wfDebug( __METHOD__ . ": showing diff page\n" );
+ $this->showDiffPage();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ # Should the parser cache be used?
+ $useParserCache = $this->useParserCache( $oldid );
+ wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
+ if ( $wgUser->getOption( 'stubthreshold' ) ) {
+ wfIncrStats( 'pcache_miss_stub' );
+ }
+
+ $wasRedirected = $this->showRedirectedFromHeader();
+ $this->showNamespaceHeader();
+
+ # Iterate through the possible ways of constructing the output text.
+ # Keep going until $outputDone is set, or we run out of things to do.
+ $pass = 0;
+ $outputDone = false;
+ $this->mParserOutput = false;
+ while ( !$outputDone && ++$pass ) {
+ switch( $pass ) {
+ case 1:
+ wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) );
+ break;
+
+ case 2:
+ # Try the parser cache
+ if ( $useParserCache ) {
+ $this->mParserOutput = $parserCache->get( $this, $parserOptions );
+ if ( $this->mParserOutput !== false ) {
+ wfDebug( __METHOD__ . ": showing parser cache contents\n" );
+ $wgOut->addParserOutput( $this->mParserOutput );
+ # Ensure that UI elements requiring revision ID have
+ # the correct version information.
+ $wgOut->setRevisionId( $this->mLatest );
+ $outputDone = true;
+ }
+ }
+ break;
+
+ case 3:
+ $text = $this->getContent();
+ if ( $text === false || $this->getID() == 0 ) {
+ wfDebug( __METHOD__ . ": showing missing article\n" );
+ $this->showMissingArticle();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ # Another whitelist check in case oldid is altering the title
+ if ( !$this->mTitle->userCanRead() ) {
+ wfDebug( __METHOD__ . ": denied on secondary read check\n" );
+ $wgOut->loginToUse();
+ $wgOut->output();
+ $wgOut->disable();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ # Are we looking at an old revision
+ if ( $oldid && !is_null( $this->mRevision ) ) {
+ $this->setOldSubtitle( $oldid );
+ if ( !$this->showDeletedRevisionHeader() ) {
+ wfDebug( __METHOD__ . ": cannot view deleted revision\n" );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ # If this "old" version is the current, then try the parser cache...
+ if ( $oldid === $this->getLatest() && $this->useParserCache( false ) ) {
+ $this->mParserOutput = $parserCache->get( $this, $parserOptions );
+ if ( $this->mParserOutput ) {
+ wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" );
+ $wgOut->addParserOutput( $this->mParserOutput );
+ $wgOut->setRevisionId( $this->mLatest );
+ $this->showViewFooter();
+ $this->viewUpdates();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ }
+ }
+
+ # Ensure that UI elements requiring revision ID have
+ # the correct version information.
+ $wgOut->setRevisionId( $this->getRevIdFetched() );
+
+ # Pages containing custom CSS or JavaScript get special treatment
+ if ( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) {
+ wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
+ $this->showCssOrJsPage();
+ $outputDone = true;
+ } else if ( $rt = Title::newFromRedirectArray( $text ) ) {
+ wfDebug( __METHOD__ . ": showing redirect=no page\n" );
+ # Viewing a redirect page (e.g. with parameter redirect=no)
+ # Don't append the subtitle if this was an old revision
+ $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) );
+ # Parse just to get categories, displaytitle, etc.
+ $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, $parserOptions );
+ $wgOut->addParserOutputNoText( $this->mParserOutput );
+ $outputDone = true;
+ }
+ break;
+
+ case 4:
+ # Run the parse, protected by a pool counter
+ wfDebug( __METHOD__ . ": doing uncached parse\n" );
+ $key = $parserCache->getKey( $this, $parserOptions );
+ $poolCounter = PoolCounter::factory( 'Article::view', $key );
+ $dirtyCallback = $useParserCache ? array( $this, 'tryDirtyCache' ) : false;
+ $status = $poolCounter->executeProtected( array( $this, 'doViewParse' ), $dirtyCallback );
+
+ if ( !$status->isOK() ) {
+ # Connection or timeout error
+ $this->showPoolError( $status );
+ wfProfileOut( __METHOD__ );
+ return;
+ } else {
+ $outputDone = true;
+ }
+ break;
+
+ # Should be unreachable, but just in case...
+ default:
+ break 2;
+ }
+ }
+
+ # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
+ if ( $this->mParserOutput ) {
+ $titleText = $this->mParserOutput->getTitleText();
+ if ( strval( $titleText ) !== '' ) {
+ $wgOut->setPageTitle( $titleText );
+ }
+ }
+
+ # For the main page, overwrite the <title> element with the con-
+ # tents of 'pagetitle-view-mainpage' instead of the default (if
+ # that's not empty).
+ if ( $this->mTitle->equals( Title::newMainPage() )
+ && ( $m = wfMsgForContent( 'pagetitle-view-mainpage' ) ) !== '' )
+ {
+ $wgOut->setHTMLTitle( $m );
+ }
+
+ # Now that we've filled $this->mParserOutput, we know whether
+ # there are any __NOINDEX__ tags on the page
+ $policy = $this->getRobotPolicy( 'view' );
+ $wgOut->setIndexPolicy( $policy['index'] );
+ $wgOut->setFollowPolicy( $policy['follow'] );
+
+ $this->showViewFooter();
+ $this->viewUpdates();
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Show a diff page according to current request variables. For use within
+ * Article::view() only, other callers should use the DifferenceEngine class.
+ */
+ public function showDiffPage() {
+ global $wgOut, $wgRequest, $wgUser;
+
$diff = $wgRequest->getVal( 'diff' );
$rcid = $wgRequest->getVal( 'rcid' );
- $rdfrom = $wgRequest->getVal( 'rdfrom' );
$diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
$purge = $wgRequest->getVal( 'action' ) == 'purge';
- $return404 = false;
+ $unhide = $wgRequest->getInt( 'unhide' ) == 1;
+ $oldid = $this->getOldID();
- $wgOut->setArticleFlag( true );
+ $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $unhide );
+ // DifferenceEngine directly fetched the revision:
+ $this->mRevIdFetched = $de->mNewid;
+ $de->showDiffPage( $diffOnly );
- # Discourage indexing of printable versions, but encourage following
- if( $wgOut->isPrintable() ) {
- $policy = 'noindex,follow';
- } elseif( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) {
- $policy = $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()];
- } elseif( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
- # Honour customised robot policies for this namespace
- $policy = $wgNamespaceRobotPolicies[$ns];
- } else {
- $policy = $wgDefaultRobotPolicy;
+ // Needed to get the page's current revision
+ $this->loadPageData();
+ if ( $diff == 0 || $diff == $this->mLatest ) {
+ # Run view updates for current revision only
+ $this->viewUpdates();
}
- $wgOut->setRobotPolicy( $policy );
+ }
- # Allow admins to see deleted content if explicitly requested
- $delId = $diff ? $diff : $oldid;
- $unhide = $wgRequest->getInt('unhide') == 1
- && $wgUser->matchEditToken( $wgRequest->getVal('token'), $delId );
- # If we got diff and oldid in the query, we want to see a
- # diff page instead of the article.
- if( !is_null( $diff ) ) {
- $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+ /**
+ * Show a page view for a page formatted as CSS or JavaScript. To be called by
+ * Article::view() only.
+ *
+ * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
+ * page views.
+ */
+ public function showCssOrJsPage() {
+ global $wgOut;
+ $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) );
+ // Give hooks a chance to customise the output
+ if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) {
+ // Wrap the whole lot in a <pre> and don't parse
+ $m = array();
+ preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+ $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+ $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
+ $wgOut->addHTML( "\n</pre>\n" );
+ }
+ }
- $htmldiff = $wgRequest->getVal( 'htmldiff' , false);
- $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff, $unhide );
- // DifferenceEngine directly fetched the revision:
- $this->mRevIdFetched = $de->mNewid;
- $de->showDiffPage( $diffOnly );
+ /**
+ * Get the robot policy to be used for the current action=view request.
+ * @return String the policy that should be set
+ * @deprecated use getRobotPolicy() instead, which returns an associative
+ * array
+ */
+ public function getRobotPolicyForView() {
+ wfDeprecated( __FUNC__ );
+ $policy = $this->getRobotPolicy( 'view' );
+ return $policy['index'] . ',' . $policy['follow'];
+ }
- // Needed to get the page's current revision
- $this->loadPageData();
- if( $diff == 0 || $diff == $this->mLatest ) {
- # Run view updates for current revision only
- $this->viewUpdates();
- }
- wfProfileOut( __METHOD__ );
- return;
- }
-
- if( $ns == NS_USER || $ns == NS_USER_TALK ) {
- # User/User_talk subpages are not modified. (bug 11443)
- if( !$this->mTitle->isSubpage() ) {
+ /**
+ * Get the robot policy to be used for the current view
+ * @param $action String the action= GET parameter
+ * @return Array the policy that should be set
+ * TODO: actions other than 'view'
+ */
+ public function getRobotPolicy( $action ) {
+
+ global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies;
+ global $wgDefaultRobotPolicy, $wgRequest;
+
+ $ns = $this->mTitle->getNamespace();
+ if ( $ns == NS_USER || $ns == NS_USER_TALK ) {
+ # Don't index user and user talk pages for blocked users (bug 11443)
+ if ( !$this->mTitle->isSubpage() ) {
$block = new Block();
- if( $block->load( $this->mTitle->getBaseText() ) ) {
- $wgOut->setRobotpolicy( 'noindex,nofollow' );
+ if ( $block->load( $this->mTitle->getText() ) ) {
+ return array( 'index' => 'noindex',
+ 'follow' => 'nofollow' );
}
}
}
- # Should the parser cache be used?
- $pcache = $this->useParserCache( $oldid );
- wfDebug( 'Article::view using parser cache: ' . ($pcache ? 'yes' : 'no' ) . "\n" );
- if( $wgUser->getOption( 'stubthreshold' ) ) {
- wfIncrStats( 'pcache_miss_stub' );
+ if ( $this->getID() === 0 || $this->getOldID() ) {
+ # Non-articles (special pages etc), and old revisions
+ return array( 'index' => 'noindex',
+ 'follow' => 'nofollow' );
+ } elseif ( $wgOut->isPrintable() ) {
+ # Discourage indexing of printable versions, but encourage following
+ return array( 'index' => 'noindex',
+ 'follow' => 'follow' );
+ } elseif ( $wgRequest->getInt( 'curid' ) ) {
+ # For ?curid=x urls, disallow indexing
+ return array( 'index' => 'noindex',
+ 'follow' => 'follow' );
}
- $wasRedirected = false;
- if( isset( $this->mRedirectedFrom ) ) {
+ # Otherwise, construct the policy based on the various config variables.
+ $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy );
+
+ if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
+ # Honour customised robot policies for this namespace
+ $policy = array_merge( $policy,
+ self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) );
+ }
+ if ( $this->mTitle->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) {
+ # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
+ # a final sanity check that we have really got the parser output.
+ $policy = array_merge( $policy,
+ array( 'index' => $this->mParserOutput->getIndexPolicy() ) );
+ }
+
+ if ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) {
+ # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__
+ $policy = array_merge( $policy,
+ self::formatRobotPolicy( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) );
+ }
+
+ return $policy;
+
+ }
+
+ /**
+ * Converts a String robot policy into an associative array, to allow
+ * merging of several policies using array_merge().
+ * @param $policy Mixed, returns empty array on null/false/'', transparent
+ * to already-converted arrays, converts String.
+ * @return associative Array: 'index' => <indexpolicy>, 'follow' => <followpolicy>
+ */
+ public static function formatRobotPolicy( $policy ) {
+ if ( is_array( $policy ) ) {
+ return $policy;
+ } elseif ( !$policy ) {
+ return array();
+ }
+
+ $policy = explode( ',', $policy );
+ $policy = array_map( 'trim', $policy );
+
+ $arr = array();
+ foreach ( $policy as $var ) {
+ if ( in_array( $var, array( 'index', 'noindex' ) ) ) {
+ $arr['index'] = $var;
+ } elseif ( in_array( $var, array( 'follow', 'nofollow' ) ) ) {
+ $arr['follow'] = $var;
+ }
+ }
+ return $arr;
+ }
+
+ /**
+ * If this request is a redirect view, send "redirected from" subtitle to
+ * $wgOut. Returns true if the header was needed, false if this is not a
+ * redirect view. Handles both local and remote redirects.
+ */
+ public function showRedirectedFromHeader() {
+ global $wgOut, $wgUser, $wgRequest, $wgRedirectSources;
+
+ $rdfrom = $wgRequest->getVal( 'rdfrom' );
+ $sk = $wgUser->getSkin();
+ if ( isset( $this->mRedirectedFrom ) ) {
// This is an internally redirected page view.
// We'll need a backlink to the source page for navigation.
- if( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) {
- $redir = $sk->makeKnownLinkObj( $this->mRedirectedFrom, '', 'redirect=no' );
+ if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) {
+ $redir = $sk->link(
+ $this->mRedirectedFrom,
+ null,
+ array(),
+ array( 'redirect' => 'no' ),
+ array( 'known', 'noclasses' )
+ );
$s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir );
$wgOut->setSubtitle( $s );
// Set the fragment if one was specified in the redirect
- if( strval( $this->mTitle->getFragment() ) != '' ) {
+ if ( strval( $this->mTitle->getFragment() ) != '' ) {
$fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() );
$wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" );
}
@@ -851,225 +1127,198 @@ class Article {
$wgOut->addLink( array( 'rel' => 'canonical',
'href' => $this->mTitle->getLocalURL() )
);
- $wasRedirected = true;
+ return true;
}
- } elseif( !empty( $rdfrom ) ) {
+ } elseif ( $rdfrom ) {
// This is an externally redirected view, from some other wiki.
// If it was reported from a trusted site, supply a backlink.
- global $wgRedirectSources;
- if( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
+ if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
$redir = $sk->makeExternalLink( $rdfrom, $rdfrom );
$s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir );
$wgOut->setSubtitle( $s );
- $wasRedirected = true;
- }
- }
-
- $outputDone = false;
- wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) );
- if( $pcache && $wgOut->tryParserCache( $this ) ) {
- // Ensure that UI elements requiring revision ID have
- // the correct version information.
- $wgOut->setRevisionId( $this->mLatest );
- $outputDone = true;
- }
- # Fetch content and check for errors
- if( !$outputDone ) {
- # If the article does not exist and was deleted, show the log
- if( $this->getID() == 0 ) {
- $this->showDeletionLog();
- }
- $text = $this->getContent();
- // For now, check also for ID until getContent actually returns
- // false for pages that do not exists
- if( $text === false || $this->getID() === 0 ) {
- # Failed to load, replace text with error message
- $t = $this->mTitle->getPrefixedText();
- if( $oldid ) {
- $d = wfMsgExt( 'missingarticle-rev', 'escape', $oldid );
- $text = wfMsgExt( 'missing-article', 'parsemag', $t, $d );
- // Always use page content for pages in the MediaWiki namespace
- // since it contains the default message
- } elseif ( $this->mTitle->getNamespace() != NS_MEDIAWIKI ) {
- $text = wfMsgExt( 'noarticletext', 'parsemag' );
- }
- }
-
- # Non-existent pages
- if( $this->getID() === 0 ) {
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- $text = "<div class='noarticletext'>\n$text\n</div>";
- if( !$this->hasViewableContent() ) {
- // If there's no backing content, send a 404 Not Found
- // for better machine handling of broken links.
- $return404 = true;
- }
- }
-
- if( $return404 ) {
- $wgRequest->response()->header( "HTTP/1.x 404 Not Found" );
- }
-
- # Another whitelist check in case oldid is altering the title
- if( !$this->mTitle->userCanRead() ) {
- $wgOut->loginToUse();
- $wgOut->output();
- $wgOut->disable();
- wfProfileOut( __METHOD__ );
- return;
- }
-
- # For ?curid=x urls, disallow indexing
- if( $wgRequest->getInt('curid') )
- $wgOut->setRobotPolicy( 'noindex,follow' );
-
- # We're looking at an old revision
- if( !empty( $oldid ) ) {
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- if( is_null( $this->mRevision ) ) {
- // FIXME: This would be a nice place to load the 'no such page' text.
- } else {
- $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid );
- # Allow admins to see deleted content if explicitly requested
- if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
- if( !$unhide || !$this->mRevision->userCan(Revision::DELETED_TEXT) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
- $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
- wfProfileOut( __METHOD__ );
- return;
- } else {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
- // and we are allowed to see...
- }
- }
- // Is this the current revision and otherwise cacheable? Try the parser cache...
- if( $oldid === $this->getLatest() && $this->useParserCache( false )
- && $wgOut->tryParserCache( $this ) )
- {
- $outputDone = true;
- }
- }
- }
-
- // Ensure that UI elements requiring revision ID have
- // the correct version information.
- $wgOut->setRevisionId( $this->getRevIdFetched() );
-
- if( $outputDone ) {
- // do nothing...
- // Pages containing custom CSS or JavaScript get special treatment
- } else if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) {
- $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) );
- // Give hooks a chance to customise the output
- if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) {
- // Wrap the whole lot in a <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
- $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
- $wgOut->addHTML( "\n</pre>\n" );
- }
- } else if( $rt = Title::newFromRedirectArray( $text ) ) { # get an array of redirect targets
- # Don't append the subtitle if this was an old revision
- $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) );
- $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser));
- $wgOut->addParserOutputNoText( $parseout );
- } else if( $pcache ) {
- # Display content and save to parser cache
- $this->outputWikiText( $text );
- } else {
- # Display content, don't attempt to save to parser cache
- # Don't show section-edit links on old revisions... this way lies madness.
- if( !$this->isCurrent() ) {
- $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
- }
- # Display content and don't save to parser cache
- # With timing hack -- TS 2006-07-26
- $time = -wfTime();
- $this->outputWikiText( $text, false );
- $time += wfTime();
-
- # Timing hack
- if( $time > 3 ) {
- wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
- $this->mTitle->getPrefixedDBkey()));
- }
-
- if( !$this->isCurrent() ) {
- $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
- }
+ return true;
}
}
- /* title may have been set from the cache */
- $t = $wgOut->getPageTitle();
- if( empty( $t ) ) {
- $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+ return false;
+ }
- # For the main page, overwrite the <title> element with the con-
- # tents of 'pagetitle-view-mainpage' instead of the default (if
- # that's not empty).
- if( $this->mTitle->equals( Title::newMainPage() ) &&
- wfMsgForContent( 'pagetitle-view-mainpage' ) !== '' ) {
- $wgOut->setHTMLTitle( wfMsgForContent( 'pagetitle-view-mainpage' ) );
+ /**
+ * Show a header specific to the namespace currently being viewed, like
+ * [[MediaWiki:Talkpagetext]]. For Article::view().
+ */
+ public function showNamespaceHeader() {
+ global $wgOut;
+ if ( $this->mTitle->isTalkPage() ) {
+ $msg = wfMsgNoTrans( 'talkpageheader' );
+ if ( $msg !== '-' && !wfEmptyMsg( 'talkpageheader', $msg ) ) {
+ $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1</div>", array( 'talkpageheader' ) );
}
}
+ }
+ /**
+ * Show the footer section of an ordinary page view
+ */
+ public function showViewFooter() {
+ global $wgOut, $wgUseTrackbacks, $wgRequest;
# check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
- if( $ns == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) {
- $wgOut->addWikiMsg('anontalkpagetext');
+ if ( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) {
+ $wgOut->addWikiMsg( 'anontalkpagetext' );
}
# If we have been passed an &rcid= parameter, we want to give the user a
# chance to mark this new article as patrolled.
- if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->quickUserCan('patrol') ) {
- $wgOut->addHTML(
- "<div class='patrollink'>" .
- wfMsgHtml( 'markaspatrolledlink',
- $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml('markaspatrolledtext'),
- "action=markpatrolled&rcid=$rcid" )
- ) .
- '</div>'
- );
- }
+ $this->showPatrolFooter();
# Trackbacks
- if( $wgUseTrackbacks ) {
+ if ( $wgUseTrackbacks ) {
$this->addTrackbacks();
}
+ }
- $this->viewUpdates();
- wfProfileOut( __METHOD__ );
+ /**
+ * If patrol is possible, output a patrol UI box. This is called from the
+ * footer section of ordinary page views. If patrol is not possible or not
+ * desired, does nothing.
+ */
+ public function showPatrolFooter() {
+ global $wgOut, $wgRequest, $wgUser;
+ $rcid = $wgRequest->getVal( 'rcid' );
+
+ if ( !$rcid || !$this->mTitle->exists() || !$this->mTitle->quickUserCan( 'patrol' ) ) {
+ return;
+ }
+
+ $sk = $wgUser->getSkin();
+
+ $wgOut->addHTML(
+ "<div class='patrollink'>" .
+ wfMsgHtml(
+ 'markaspatrolledlink',
+ $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'markaspatrolledtext' ),
+ array(),
+ array(
+ 'action' => 'markpatrolled',
+ 'rcid' => $rcid
+ ),
+ array( 'known', 'noclasses' )
+ )
+ ) .
+ '</div>'
+ );
}
-
- protected function showDeletionLog() {
- global $wgUser, $wgOut;
- $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut );
- $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() );
- if( $pager->getNumRows() > 0 ) {
- $pager->mLimit = 10;
- $wgOut->addHTML( '<div class="mw-warning-with-logexcerpt">' );
- $wgOut->addWikiMsg( 'deleted-notice' );
- $wgOut->addHTML(
- $loglist->beginLogEventsList() .
- $pager->getBody() .
- $loglist->endLogEventsList()
- );
- if( $pager->getNumRows() > 10 ) {
- $wgOut->addHTML( $wgUser->getSkin()->link(
- SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'deletelog-fulllog' ),
- array(),
- array( 'type' => 'delete', 'page' => $this->mTitle->getPrefixedText() )
- ) );
+
+ /**
+ * Show the error text for a missing article. For articles in the MediaWiki
+ * namespace, show the default message text. To be called from Article::view().
+ */
+ public function showMissingArticle() {
+ global $wgOut, $wgRequest, $wgUser;
+
+ # Show info in user (talk) namespace. Does the user exist? Is he blocked?
+ if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
+ $parts = explode( '/', $this->mTitle->getText() );
+ $rootPart = $parts[0];
+ $user = User::newFromName( $rootPart, false /* allow IP users*/ );
+ $ip = User::isIP( $rootPart );
+ if ( !$user->isLoggedIn() && !$ip ) { # User does not exist
+ $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1</div>",
+ array( 'userpage-userdoesnotexist-view', $rootPart ) );
+ } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'block',
+ $user->getUserPage()->getPrefixedText(),
+ '',
+ array(
+ 'lim' => 1,
+ 'showIfEmpty' => false,
+ 'msgKey' => array(
+ 'blocked-notice-logextract',
+ $user->getName() # Support GENDER in notice
+ )
+ )
+ );
}
- $wgOut->addHTML( '</div>' );
+ }
+ wfRunHooks( 'ShowMissingArticle', array( $this ) );
+ # Show delete and move logs
+ LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(), '',
+ array( 'lim' => 10,
+ 'conds' => array( "log_action != 'revision'" ),
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'moveddeleted-notice' ) )
+ );
+
+ # Show error message
+ $oldid = $this->getOldID();
+ if ( $oldid ) {
+ $text = wfMsgNoTrans( 'missing-article',
+ $this->mTitle->getPrefixedText(),
+ wfMsgNoTrans( 'missingarticle-rev', $oldid ) );
+ } elseif ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
+ // Use the default message text
+ $text = $this->getContent();
+ } else {
+ $createErrors = $this->mTitle->getUserPermissionsErrors( 'create', $wgUser );
+ $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
+ $errors = array_merge( $createErrors, $editErrors );
+
+ if ( !count( $errors ) )
+ $text = wfMsgNoTrans( 'noarticletext' );
+ else
+ $text = wfMsgNoTrans( 'noarticletext-nopermission' );
+ }
+ $text = "<div class='noarticletext'>\n$text\n</div>";
+ if ( !$this->hasViewableContent() ) {
+ // If there's no backing content, send a 404 Not Found
+ // for better machine handling of broken links.
+ $wgRequest->response()->header( "HTTP/1.x 404 Not Found" );
+ }
+ $wgOut->addWikiText( $text );
+ }
+
+ /**
+ * If the revision requested for view is deleted, check permissions.
+ * Send either an error message or a warning header to $wgOut.
+ * Returns true if the view is allowed, false if not.
+ */
+ public function showDeletedRevisionHeader() {
+ global $wgOut, $wgRequest;
+ if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
+ // Not deleted
+ return true;
+ }
+ // If the user is not allowed to see it...
+ if ( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
+ 'rev-deleted-text-permission' );
+ return false;
+ // If the user needs to confirm that they want to see it...
+ } else if ( $wgRequest->getInt( 'unhide' ) != 1 ) {
+ # Give explanation and add a link to view the revision...
+ $oldid = intval( $this->getOldID() );
+ $link = $this->mTitle->getFullUrl( "oldid={$oldid}&unhide=1" );
+ $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
+ 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
+ array( $msg, $link ) );
+ return false;
+ // We are allowed to see...
+ } else {
+ $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
+ 'rev-suppressed-text-view' : 'rev-deleted-text-view';
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", $msg );
+ return true;
}
}
/*
* Should the parser cache be used?
*/
- protected function useParserCache( $oldid ) {
+ public function useParserCache( $oldid ) {
global $wgUser, $wgEnableParserCache;
return $wgEnableParserCache
@@ -1081,45 +1330,116 @@ class Article {
}
/**
+ * Execute the uncached parse for action=view
+ */
+ public function doViewParse() {
+ global $wgOut;
+ $oldid = $this->getOldID();
+ $useParserCache = $this->useParserCache( $oldid );
+ $parserOptions = clone $this->getParserOptions();
+ # Render printable version, use printable version cache
+ $parserOptions->setIsPrintable( $wgOut->isPrintable() );
+ # Don't show section-edit links on old revisions... this way lies madness.
+ $parserOptions->setEditSection( $this->isCurrent() );
+ $useParserCache = $this->useParserCache( $oldid );
+ $this->outputWikiText( $this->getContent(), $useParserCache, $parserOptions );
+ }
+
+ /**
+ * Try to fetch an expired entry from the parser cache. If it is present,
+ * output it and return true. If it is not present, output nothing and
+ * return false. This is used as a callback function for
+ * PoolCounter::executeProtected().
+ */
+ public function tryDirtyCache() {
+ global $wgOut;
+ $parserCache = ParserCache::singleton();
+ $options = $this->getParserOptions();
+ $options->setIsPrintable( $wgOut->isPrintable() );
+ $output = $parserCache->getDirty( $this, $options );
+ if ( $output ) {
+ wfDebug( __METHOD__ . ": sending dirty output\n" );
+ wfDebugLog( 'dirty', "dirty output " . $parserCache->getKey( $this, $options ) . "\n" );
+ $wgOut->setSquidMaxage( 0 );
+ $this->mParserOutput = $output;
+ $wgOut->addParserOutput( $output );
+ $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" );
+ return true;
+ } else {
+ wfDebugLog( 'dirty', "dirty missing\n" );
+ wfDebug( __METHOD__ . ": no dirty cache\n" );
+ return false;
+ }
+ }
+
+ /**
+ * Show an error page for an error from the pool counter.
+ * @param $status Status
+ */
+ public function showPoolError( $status ) {
+ global $wgOut;
+ $wgOut->clearHTML(); // for release() errors
+ $wgOut->enableClientCache( false );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->addWikiText(
+ '<div class="errorbox">' .
+ $status->getWikiText( false, 'view-pool-error' ) .
+ '</div>'
+ );
+ }
+
+ /**
* View redirect
* @param $target Title object or Array of destination(s) to redirect
* @param $appendSubtitle Boolean [optional]
* @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence?
*/
public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
- global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser;
+ global $wgOut, $wgContLang, $wgStylePath, $wgUser;
# Display redirect
- if( !is_array( $target ) ) {
+ if ( !is_array( $target ) ) {
$target = array( $target );
}
- $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
+ $imageDir = $wgContLang->getDir();
$imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png';
$imageUrl2 = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png';
$alt2 = $wgContLang->isRTL() ? '&larr;' : '&rarr;'; // should -> and <- be used instead of entities?
-
- if( $appendSubtitle ) {
+
+ if ( $appendSubtitle ) {
$wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) );
}
$sk = $wgUser->getSkin();
// the loop prepends the arrow image before the link, so the first case needs to be outside
$title = array_shift( $target );
- if( $forceKnown ) {
- $link = $sk->makeKnownLinkObj( $title, htmlspecialchars( $title->getFullText() ) );
+ if ( $forceKnown ) {
+ $link = $sk->link(
+ $title,
+ htmlspecialchars( $title->getFullText() ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
} else {
- $link = $sk->makeLinkObj( $title, htmlspecialchars( $title->getFullText() ) );
+ $link = $sk->link( $title, htmlspecialchars( $title->getFullText() ) );
}
// automatically append redirect=no to each link, since most of them are redirect pages themselves
- foreach( $target as $rt ) {
- if( $forceKnown ) {
- $link .= '<img src="'.$imageUrl2.'" alt="'.$alt2.' " />'
- . $sk->makeKnownLinkObj( $rt, htmlspecialchars( $rt->getFullText() ) );
+ foreach ( $target as $rt ) {
+ if ( $forceKnown ) {
+ $link .= '<img src="' . $imageUrl2 . '" alt="' . $alt2 . ' " />'
+ . $sk->link(
+ $rt,
+ htmlspecialchars( $rt->getFullText() ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
} else {
- $link .= '<img src="'.$imageUrl2.'" alt="'.$alt2.' " />'
- . $sk->makeLinkObj( $rt, htmlspecialchars( $rt->getFullText() ) );
+ $link .= '<img src="' . $imageUrl2 . '" alt="' . $alt2 . ' " />'
+ . $sk->link( $rt, htmlspecialchars( $rt->getFullText() ) );
}
}
- return '<img src="'.$imageUrl.'" alt="#REDIRECT " />' .
- '<span class="redirectText">'.$link.'</span>';
+ return '<img src="' . $imageUrl . '" alt="#REDIRECT " />' .
+ '<span class="redirectText">' . $link . '</span>';
}
@@ -1127,46 +1447,46 @@ class Article {
global $wgOut, $wgUser;
$dbr = wfGetDB( DB_SLAVE );
$tbs = $dbr->select( 'trackbacks',
- array('tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name'),
- array('tb_page' => $this->getID() )
+ array( 'tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name' ),
+ array( 'tb_page' => $this->getID() )
);
- if( !$dbr->numRows($tbs) ) return;
+ if ( !$dbr->numRows( $tbs ) ) return;
$tbtext = "";
- while( $o = $dbr->fetchObject($tbs) ) {
+ while ( $o = $dbr->fetchObject( $tbs ) ) {
$rmvtxt = "";
- if( $wgUser->isAllowed( 'trackback' ) ) {
- $delurl = $this->mTitle->getFullURL("action=deletetrackback&tbid=" .
+ if ( $wgUser->isAllowed( 'trackback' ) ) {
+ $delurl = $this->mTitle->getFullURL( "action=deletetrackback&tbid=" .
$o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
$rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) );
}
$tbtext .= "\n";
- $tbtext .= wfMsg(strlen($o->tb_ex) ? 'trackbackexcerpt' : 'trackback',
+ $tbtext .= wfMsg( strlen( $o->tb_ex ) ? 'trackbackexcerpt' : 'trackback',
$o->tb_title,
$o->tb_url,
$o->tb_ex,
$o->tb_name,
- $rmvtxt);
+ $rmvtxt );
}
$wgOut->wrapWikiMsg( "<div id='mw_trackbacks'>$1</div>\n", array( 'trackbackbox', $tbtext ) );
$this->mTitle->invalidateCache();
}
public function deletetrackback() {
- global $wgUser, $wgRequest, $wgOut, $wgTitle;
- if( !$wgUser->matchEditToken($wgRequest->getVal('token')) ) {
+ global $wgUser, $wgRequest, $wgOut;
+ if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
$wgOut->addWikiMsg( 'sessionfailure' );
return;
}
$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
- if( count($permission_errors) ) {
+ if ( count( $permission_errors ) ) {
$wgOut->showPermissionsErrorPage( $permission_errors );
return;
}
$db = wfGetDB( DB_MASTER );
- $db->delete( 'trackbacks', array('tb_id' => $wgRequest->getInt('tbid')) );
+ $db->delete( 'trackbacks', array( 'tb_id' => $wgRequest->getInt( 'tbid' ) ) );
$wgOut->addWikiMsg( 'trackbackdeleteok' );
$this->mTitle->invalidateCache();
@@ -1174,7 +1494,7 @@ class Article {
public function render() {
global $wgOut;
- $wgOut->setArticleBodyOnly(true);
+ $wgOut->setArticleBodyOnly( true );
$this->view();
}
@@ -1183,19 +1503,19 @@ class Article {
*/
public function purge() {
global $wgUser, $wgRequest, $wgOut;
- if( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) {
- if( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
+ if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) {
+ if ( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
$this->doPurge();
$this->view();
}
} else {
$action = htmlspecialchars( $wgRequest->getRequestURL() );
- $button = wfMsgExt( 'confirm_purge_button', array('escapenoentities') );
+ $button = wfMsgExt( 'confirm_purge_button', array( 'escapenoentities' ) );
$form = "<form method=\"post\" action=\"$action\">\n" .
"<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" .
"</form>\n";
- $top = wfMsgExt( 'confirm-purge-top', array('parse') );
- $bottom = wfMsgExt( 'confirm-purge-bottom', array('parse') );
+ $top = wfMsgExt( 'confirm-purge-top', array( 'parse' ) );
+ $bottom = wfMsgExt( 'confirm-purge-bottom', array( 'parse' ) );
$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addHTML( $top . $form . $bottom );
@@ -1210,18 +1530,18 @@ class Article {
// Invalidate the cache
$this->mTitle->invalidateCache();
- if( $wgUseSquid ) {
+ if ( $wgUseSquid ) {
// Commit the transaction before the purge is sent
$dbw = wfGetDB( DB_MASTER );
- $dbw->immediateCommit();
+ $dbw->commit();
// Send purge
$update = SquidUpdate::newSimplePurge( $this->mTitle );
$update->doUpdate();
}
- if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
global $wgMessageCache;
- if( $this->getID() == 0 ) {
+ if ( $this->getID() == 0 ) {
$text = false;
} else {
$text = $this->getRawText();
@@ -1260,7 +1580,7 @@ class Article {
), __METHOD__, 'IGNORE' );
$affected = $dbw->affectedRows();
- if( $affected ) {
+ if ( $affected ) {
$newid = $dbw->insertId();
$this->mTitle->resetArticleId( $newid );
}
@@ -1279,7 +1599,7 @@ class Article {
* Giving 0 indicates the new page flag should be set
* on.
* @param $lastRevIsRedirect Boolean: if given, will optimize adding and
- * removing rows in redirect table.
+ * removing rows in redirect table.
* @return bool true on success, false on failure
* @private
*/
@@ -1290,7 +1610,7 @@ class Article {
$rt = Title::newFromRedirect( $text );
$conditions = array( 'page_id' => $this->getId() );
- if( !is_null( $lastRevision ) ) {
+ if ( !is_null( $lastRevision ) ) {
# An extra check against threads stepping on each other
$conditions['page_latest'] = $lastRevision;
}
@@ -1299,15 +1619,15 @@ class Article {
array( /* SET */
'page_latest' => $revision->getId(),
'page_touched' => $dbw->timestamp(),
- 'page_is_new' => ($lastRevision === 0) ? 1 : 0,
- 'page_is_redirect' => $rt !== NULL ? 1 : 0,
+ 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
+ 'page_is_redirect' => $rt !== null ? 1 : 0,
'page_len' => strlen( $text ),
),
$conditions,
__METHOD__ );
$result = $dbw->affectedRows() != 0;
- if( $result ) {
+ if ( $result ) {
$this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
}
@@ -1320,9 +1640,9 @@ class Article {
*
* @param $dbw Database
* @param $redirectTitle a title object pointing to the redirect target,
- * or NULL if this is not a redirect
+ * or NULL if this is not a redirect
* @param $lastRevIsRedirect If given, will optimize adding and
- * removing rows in redirect table.
+ * removing rows in redirect table.
* @return bool true on success, false on failure
* @private
*/
@@ -1330,10 +1650,10 @@ class Article {
// Always update redirects (target link might have changed)
// Update/Insert if we don't know if the last revision was a redirect or not
// Delete if changing from redirect to non-redirect
- $isRedirect = !is_null($redirectTitle);
- if($isRedirect || is_null($lastRevIsRedirect) || $lastRevIsRedirect !== $isRedirect) {
+ $isRedirect = !is_null( $redirectTitle );
+ if ( $isRedirect || is_null( $lastRevIsRedirect ) || $lastRevIsRedirect !== $isRedirect ) {
wfProfileIn( __METHOD__ );
- if( $isRedirect ) {
+ if ( $isRedirect ) {
// This title is a redirect, Add/Update row in the redirect table
$set = array( /* SET */
'rd_namespace' => $redirectTitle->getNamespace(),
@@ -1344,9 +1664,9 @@ class Article {
} else {
// This is not a redirect, remove row from redirect table
$where = array( 'rd_from' => $this->getId() );
- $dbw->delete( 'redirect', $where, __METHOD__);
+ $dbw->delete( 'redirect', $where, __METHOD__ );
}
- if( $this->getTitle()->getNamespace() == NS_FILE ) {
+ if ( $this->getTitle()->getNamespace() == NS_FILE ) {
RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
}
wfProfileOut( __METHOD__ );
@@ -1371,8 +1691,8 @@ class Article {
'page_id' => $this->getId(),
'page_latest=rev_id' ),
__METHOD__ );
- if( $row ) {
- if( wfTimestamp(TS_MW, $row->rev_timestamp) >= $revision->getTimestamp() ) {
+ if ( $row ) {
+ if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
wfProfileOut( __METHOD__ );
return false;
}
@@ -1392,27 +1712,27 @@ class Article {
* @param $section empty/null/false or a section number (0, 1, 2, T1, T2...)
* @return string Complete article text, or null if error
*/
- public function replaceSection( $section, $text, $summary = '', $edittime = NULL ) {
+ public function replaceSection( $section, $text, $summary = '', $edittime = null ) {
wfProfileIn( __METHOD__ );
- if( strval( $section ) == '' ) {
+ if ( strval( $section ) == '' ) {
// Whole-page edit; let the whole text through
} else {
- if( is_null($edittime) ) {
+ if ( is_null( $edittime ) ) {
$rev = Revision::newFromTitle( $this->mTitle );
} else {
$dbw = wfGetDB( DB_MASTER );
$rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
}
- if( !$rev ) {
+ if ( !$rev ) {
wfDebug( "Article::replaceSection asked for bogus section (page: " .
$this->getId() . "; section: $section; edittime: $edittime)\n" );
return null;
}
$oldtext = $rev->getText();
- if( $section == 'new' ) {
+ if ( $section == 'new' ) {
# Inserting a new section
- $subject = $summary ? wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n" : '';
+ $subject = $summary ? wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" : '';
$text = strlen( trim( $oldtext ) ) > 0
? "{$oldtext}\n\n{$subject}{$text}"
: "{$subject}{$text}";
@@ -1427,31 +1747,31 @@ class Article {
}
/**
- * @deprecated use Article::doEdit()
+ * This function is not deprecated until somebody fixes the core not to use
+ * it. Nevertheless, use Article::doEdit() instead.
*/
- function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC=false, $comment=false, $bot=false ) {
- wfDeprecated( __METHOD__ );
+ function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC = false, $comment = false, $bot = false ) {
$flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
( $isminor ? EDIT_MINOR : 0 ) |
( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) |
( $bot ? EDIT_FORCE_BOT : 0 );
# If this is a comment, add the summary as headline
- if( $comment && $summary != "" ) {
- $text = wfMsgForContent('newsectionheaderdefaultlevel',$summary) . "\n\n".$text;
+ if ( $comment && $summary != "" ) {
+ $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" . $text;
}
$this->doEdit( $text, $summary, $flags );
$dbw = wfGetDB( DB_MASTER );
- if($watchthis) {
- if(!$this->mTitle->userIsWatching()) {
+ if ( $watchthis ) {
+ if ( !$this->mTitle->userIsWatching() ) {
$dbw->begin();
$this->doWatch();
$dbw->commit();
}
} else {
- if( $this->mTitle->userIsWatching() ) {
+ if ( $this->mTitle->userIsWatching() ) {
$dbw->begin();
$this->doUnwatch();
$dbw->commit();
@@ -1464,25 +1784,24 @@ class Article {
* @deprecated use Article::doEdit()
*/
function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) {
- wfDeprecated( __METHOD__ );
$flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
( $minor ? EDIT_MINOR : 0 ) |
( $forceBot ? EDIT_FORCE_BOT : 0 );
$status = $this->doEdit( $text, $summary, $flags );
- if( !$status->isOK() ) {
+ if ( !$status->isOK() ) {
return false;
}
$dbw = wfGetDB( DB_MASTER );
- if( $watchthis ) {
- if(!$this->mTitle->userIsWatching()) {
+ if ( $watchthis ) {
+ if ( !$this->mTitle->userIsWatching() ) {
$dbw->begin();
$this->doWatch();
$dbw->commit();
}
} else {
- if( $this->mTitle->userIsWatching() ) {
+ if ( $this->mTitle->userIsWatching() ) {
$dbw->begin();
$this->doUnwatch();
$dbw->commit();
@@ -1523,9 +1842,9 @@ class Article {
* Fill in blank summaries with generated text where possible
*
* If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
- * If EDIT_UPDATE is specified and the article doesn't exist, the function will an
- * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
- * edit-already-exists error will be returned. These two conditions are also possible with
+ * If EDIT_UPDATE is specified and the article doesn't exist, the function will an
+ * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
+ * edit-already-exists error will be returned. These two conditions are also possible with
* auto-detection due to MediaWiki's performance-optimised locking strategy.
*
* @param $baseRevId the revision ID this edit was based off, if any
@@ -1550,47 +1869,47 @@ class Article {
global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
# Low-level sanity check
- if( $this->mTitle->getText() == '' ) {
+ if ( $this->mTitle->getText() == '' ) {
throw new MWException( 'Something is trying to edit an article with an empty title' );
}
wfProfileIn( __METHOD__ );
- $user = is_null($user) ? $wgUser : $user;
+ $user = is_null( $user ) ? $wgUser : $user;
$status = Status::newGood( array() );
# Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
- $this->loadPageData();
+ $this->loadPageData();
- if( !($flags & EDIT_NEW) && !($flags & EDIT_UPDATE) ) {
+ if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
$aid = $this->mTitle->getArticleID();
- if( $aid ) {
+ if ( $aid ) {
$flags |= EDIT_UPDATE;
} else {
$flags |= EDIT_NEW;
}
}
- if( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
+ if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
$flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
{
wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
wfProfileOut( __METHOD__ );
- if( $status->isOK() ) {
- $status->fatal( 'edit-hook-aborted');
+ if ( $status->isOK() ) {
+ $status->fatal( 'edit-hook-aborted' );
}
return $status;
}
# Silently ignore EDIT_MINOR if not allowed
- $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed('minoredit');
+ $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
$bot = $flags & EDIT_FORCE_BOT;
$oldtext = $this->getRawText(); // current revision
$oldsize = strlen( $oldtext );
# Provide autosummaries if one is not provided and autosummaries are enabled.
- if( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
+ if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
$summary = $this->getAutosummary( $oldtext, $text, $flags );
}
@@ -1600,12 +1919,13 @@ class Article {
$dbw = wfGetDB( DB_MASTER );
$now = wfTimestampNow();
+ $this->mTimestamp = $now;
- if( $flags & EDIT_UPDATE ) {
+ if ( $flags & EDIT_UPDATE ) {
# Update article, but only if changed.
$status->value['new'] = false;
# Make sure the revision is either completely inserted or not inserted at all
- if( !$wgDBtransactions ) {
+ if ( !$wgDBtransactions ) {
$userAbort = ignore_user_abort( true );
}
@@ -1613,14 +1933,14 @@ class Article {
$changed = ( strcmp( $text, $oldtext ) != 0 );
- if( $changed ) {
+ if ( $changed ) {
$this->mGoodAdjustment = (int)$this->isCountable( $text )
- (int)$this->isCountable( $oldtext );
$this->mTotalAdjustment = 0;
- if( !$this->mLatest ) {
+ if ( !$this->mLatest ) {
# Article gone missing
- wfDebug( __METHOD__.": EDIT_UPDATE specified but article doesn't exist\n" );
+ wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
$status->fatal( 'edit-gone-missing' );
wfProfileOut( __METHOD__ );
return $status;
@@ -1641,36 +1961,36 @@ class Article {
# Update page
#
- # Note that we use $this->mLatest instead of fetching a value from the master DB
- # during the course of this function. This makes sure that EditPage can detect
- # edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
+ # Note that we use $this->mLatest instead of fetching a value from the master DB
+ # during the course of this function. This makes sure that EditPage can detect
+ # edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
# before this function is called. A previous function used a separate query, this
# creates a window where concurrent edits can cause an ignored edit conflict.
$ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest );
- if( !$ok ) {
+ if ( !$ok ) {
/* Belated edit conflict! Run away!! */
$status->fatal( 'edit-conflict' );
# Delete the invalid revision if the DB is not transactional
- if( !$wgDBtransactions ) {
+ if ( !$wgDBtransactions ) {
$dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ );
}
$revisionId = 0;
$dbw->rollback();
} else {
global $wgUseRCPatrol;
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, $baseRevId, $user) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
# Update recentchanges
- if( !( $flags & EDIT_SUPPRESS_RC ) ) {
+ if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
# Mark as patrolled if the user can do so
- $patrolled = $wgUseRCPatrol && $this->mTitle->userCan('autopatrol');
+ $patrolled = $wgUseRCPatrol && $this->mTitle->userCan( 'autopatrol' );
# Add RC row to the DB
$rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
$this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
$revisionId, $patrolled
);
# Log auto-patrolled edits
- if( $patrolled ) {
+ if ( $patrolled ) {
PatrolLog::record( $rc, true );
}
}
@@ -1687,11 +2007,11 @@ class Article {
$this->mTitle->invalidateCache();
}
- if( !$wgDBtransactions ) {
+ if ( !$wgDBtransactions ) {
ignore_user_abort( $userAbort );
}
// Now that ignore_user_abort is restored, we can respond to fatal errors
- if( !$status->isOK() ) {
+ if ( !$status->isOK() ) {
wfProfileOut( __METHOD__ );
return $status;
}
@@ -1717,7 +2037,7 @@ class Article {
# This will return false if the article already exists
$newid = $this->insertOn( $dbw );
- if( $newid === false ) {
+ if ( $newid === false ) {
$dbw->rollback();
$status->fatal( 'edit-already-exists' );
wfProfileOut( __METHOD__ );
@@ -1740,17 +2060,17 @@ class Article {
# Update the page record with revision data
$this->updateRevisionOn( $dbw, $revision, 0 );
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $user) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
# Update recentchanges
- if( !( $flags & EDIT_SUPPRESS_RC ) ) {
+ if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
global $wgUseRCPatrol, $wgUseNPPatrol;
# Mark as patrolled if the user can do so
- $patrolled = ($wgUseRCPatrol || $wgUseNPPatrol) && $this->mTitle->userCan('autopatrol');
+ $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && $this->mTitle->userCan( 'autopatrol' );
# Add RC row to the DB
$rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
- '', strlen($text), $revisionId, $patrolled );
+ '', strlen( $text ), $revisionId, $patrolled );
# Log auto-patrolled edits
- if( $patrolled ) {
+ if ( $patrolled ) {
PatrolLog::record( $rc, true );
}
}
@@ -1768,7 +2088,7 @@ class Article {
}
# Do updates right now unless deferral was requested
- if( !( $flags & EDIT_DEFER_UPDATES ) ) {
+ if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
wfDoUpdates();
}
@@ -1800,9 +2120,9 @@ class Article {
*/
public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
global $wgOut;
- if( $noRedir ) {
+ if ( $noRedir ) {
$query = 'redirect=no';
- if( $extraQuery )
+ if ( $extraQuery )
$query .= "&$query";
} else {
$query = $extraQuery;
@@ -1818,63 +2138,62 @@ class Article {
$wgOut->setRobotPolicy( 'noindex,nofollow' );
# If we haven't been given an rc_id value, we can't do anything
- $rcid = (int) $wgRequest->getVal('rcid');
- $rc = RecentChange::newFromId($rcid);
- if( is_null($rc) ) {
+ $rcid = (int) $wgRequest->getVal( 'rcid' );
+ $rc = RecentChange::newFromId( $rcid );
+ if ( is_null( $rc ) ) {
$wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
return;
}
- #It would be nice to see where the user had actually come from, but for now just guess
+ # It would be nice to see where the user had actually come from, but for now just guess
$returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
$return = SpecialPage::getTitleFor( $returnto );
$dbw = wfGetDB( DB_MASTER );
$errors = $rc->doMarkPatrolled();
- if( in_array(array('rcpatroldisabled'), $errors) ) {
+ if ( in_array( array( 'rcpatroldisabled' ), $errors ) ) {
$wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
return;
}
-
- if( in_array(array('hookaborted'), $errors) ) {
+
+ if ( in_array( array( 'hookaborted' ), $errors ) ) {
// The hook itself has handled any output
return;
}
-
- if( in_array(array('markedaspatrollederror-noautopatrol'), $errors) ) {
+
+ if ( in_array( array( 'markedaspatrollederror-noautopatrol' ), $errors ) ) {
$wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
$wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
$wgOut->returnToMain( false, $return );
return;
}
- if( !empty($errors) ) {
+ if ( !empty( $errors ) ) {
$wgOut->showPermissionsErrorPage( $errors );
return;
}
# Inform the user
$wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) );
- $wgOut->addWikiMsg( 'markedaspatrolledtext' );
+ $wgOut->addWikiMsg( 'markedaspatrolledtext', $rc->getTitle()->getPrefixedText() );
$wgOut->returnToMain( false, $return );
}
/**
* User-interface handler for the "watch" action
*/
-
public function watch() {
global $wgUser, $wgOut;
- if( $wgUser->isAnon() ) {
+ if ( $wgUser->isAnon() ) {
$wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
return;
}
- if( wfReadOnly() ) {
+ if ( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
- if( $this->doWatch() ) {
+ if ( $this->doWatch() ) {
$wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
@@ -1888,12 +2207,12 @@ class Article {
*/
public function doWatch() {
global $wgUser;
- if( $wgUser->isAnon() ) {
+ if ( $wgUser->isAnon() ) {
return false;
}
- if( wfRunHooks('WatchArticle', array(&$wgUser, &$this)) ) {
+ if ( wfRunHooks( 'WatchArticle', array( &$wgUser, &$this ) ) ) {
$wgUser->addWatch( $this->mTitle );
- return wfRunHooks('WatchArticleComplete', array(&$wgUser, &$this));
+ return wfRunHooks( 'WatchArticleComplete', array( &$wgUser, &$this ) );
}
return false;
}
@@ -1903,15 +2222,15 @@ class Article {
*/
public function unwatch() {
global $wgUser, $wgOut;
- if( $wgUser->isAnon() ) {
+ if ( $wgUser->isAnon() ) {
$wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
return;
}
- if( wfReadOnly() ) {
+ if ( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
- if( $this->doUnwatch() ) {
+ if ( $this->doUnwatch() ) {
$wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
@@ -1925,12 +2244,12 @@ class Article {
*/
public function doUnwatch() {
global $wgUser;
- if( $wgUser->isAnon() ) {
+ if ( $wgUser->isAnon() ) {
return false;
}
- if( wfRunHooks('UnwatchArticle', array(&$wgUser, &$this)) ) {
+ if ( wfRunHooks( 'UnwatchArticle', array( &$wgUser, &$this ) ) ) {
$wgUser->removeWatch( $this->mTitle );
- return wfRunHooks('UnwatchArticleComplete', array(&$wgUser, &$this));
+ return wfRunHooks( 'UnwatchArticleComplete', array( &$wgUser, &$this ) );
}
return false;
}
@@ -1960,25 +2279,27 @@ class Article {
* @return bool true on success
*/
public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) {
- global $wgUser, $wgRestrictionTypes, $wgContLang;
+ global $wgUser, $wgContLang;
+
+ $restrictionTypes = $this->mTitle->getRestrictionTypes();
$id = $this->mTitle->getArticleID();
if ( $id <= 0 ) {
wfDebug( "updateRestrictions failed: $id <= 0\n" );
return false;
}
-
+
if ( wfReadOnly() ) {
wfDebug( "updateRestrictions failed: read-only\n" );
return false;
}
-
+
if ( !$this->mTitle->userCan( 'protect' ) ) {
wfDebug( "updateRestrictions failed: insufficient permissions\n" );
return false;
}
- if( !$cascade ) {
+ if ( !$cascade ) {
$cascade = false;
}
@@ -1990,17 +2311,17 @@ class Article {
$current = array();
$updated = Article::flattenRestrictions( $limit );
$changed = false;
- foreach( $wgRestrictionTypes as $action ) {
- if( isset( $expiry[$action] ) ) {
+ foreach ( $restrictionTypes as $action ) {
+ if ( isset( $expiry[$action] ) ) {
# Get current restrictions on $action
$aLimits = $this->mTitle->getRestrictions( $action );
$current[$action] = implode( '', $aLimits );
# Are any actual restrictions being dealt with here?
- $aRChanged = count($aLimits) || !empty($limit[$action]);
+ $aRChanged = count( $aLimits ) || !empty( $limit[$action] );
# If something changed, we need to log it. Checking $aRChanged
# assures that "unprotecting" a page that is not protected does
# not log just because the expiry was "changed".
- if( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) {
+ if ( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) {
$changed = true;
}
}
@@ -2008,19 +2329,19 @@ class Article {
$current = Article::flattenRestrictions( $current );
- $changed = ($changed || $current != $updated );
- $changed = $changed || ($updated && $this->mTitle->areRestrictionsCascading() != $cascade);
+ $changed = ( $changed || $current != $updated );
+ $changed = $changed || ( $updated && $this->mTitle->areRestrictionsCascading() != $cascade );
$protect = ( $updated != '' );
# If nothing's changed, do nothing
- if( $changed ) {
- if( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
+ if ( $changed ) {
+ if ( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
$dbw = wfGetDB( DB_MASTER );
-
+
# Prepare a null revision to be added to the history
$modified = $current != '' && $protect;
- if( $protect ) {
+ if ( $protect ) {
$comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle';
} else {
$comment_type = 'unprotectedarticle';
@@ -2031,51 +2352,51 @@ class Article {
# Otherwise, people who cannot normally protect can "protect" pages via transclusion
$editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
# The schema allows multiple restrictions
- if(!in_array('protect', $editrestriction) && !in_array('sysop', $editrestriction))
+ if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) )
$cascade = false;
- $cascade_description = '';
- if( $cascade ) {
- $cascade_description = ' ['.wfMsgForContent('protect-summary-cascade').']';
+ $cascade_description = '';
+ if ( $cascade ) {
+ $cascade_description = ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']';
}
- if( $reason )
+ if ( $reason )
$comment .= ": $reason";
$editComment = $comment;
$encodedExpiry = array();
$protect_description = '';
- foreach( $limit as $action => $restrictions ) {
- if ( !isset($expiry[$action]) )
+ foreach ( $limit as $action => $restrictions ) {
+ if ( !isset( $expiry[$action] ) )
$expiry[$action] = 'infinite';
-
- $encodedExpiry[$action] = Block::encodeExpiry($expiry[$action], $dbw );
- if( $restrictions != '' ) {
+
+ $encodedExpiry[$action] = Block::encodeExpiry( $expiry[$action], $dbw );
+ if ( $restrictions != '' ) {
$protect_description .= "[$action=$restrictions] (";
- if( $encodedExpiry[$action] != 'infinity' ) {
- $protect_description .= wfMsgForContent( 'protect-expiring',
+ if ( $encodedExpiry[$action] != 'infinity' ) {
+ $protect_description .= wfMsgForContent( 'protect-expiring',
$wgContLang->timeanddate( $expiry[$action], false, false ) ,
$wgContLang->date( $expiry[$action], false, false ) ,
- $wgContLang->time( $expiry[$action], false, false ) );
+ $wgContLang->time( $expiry[$action], false, false ) );
} else {
$protect_description .= wfMsgForContent( 'protect-expiry-indefinite' );
}
$protect_description .= ') ';
}
}
- $protect_description = trim($protect_description);
-
- if( $protect_description && $protect )
+ $protect_description = trim( $protect_description );
+
+ if ( $protect_description && $protect )
$editComment .= " ($protect_description)";
- if( $cascade )
+ if ( $cascade )
$editComment .= "$cascade_description";
# Update restrictions table
- foreach( $limit as $action => $restrictions ) {
- if($restrictions != '' ) {
- $dbw->replace( 'page_restrictions', array(array('pr_page', 'pr_type')),
- array( 'pr_page' => $id,
- 'pr_type' => $action,
- 'pr_level' => $restrictions,
- 'pr_cascade' => ($cascade && $action == 'edit') ? 1 : 0,
+ foreach ( $limit as $action => $restrictions ) {
+ if ( $restrictions != '' ) {
+ $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ),
+ array( 'pr_page' => $id,
+ 'pr_type' => $action,
+ 'pr_level' => $restrictions,
+ 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
'pr_expiry' => $encodedExpiry[$action] ), __METHOD__ );
} else {
$dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
@@ -2099,14 +2420,14 @@ class Article {
), 'Article::protect'
);
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $nullRevision, $latest, $wgUser) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $wgUser ) );
wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
# Update the protection log
$log = new LogPage( 'protect' );
- if( $protect ) {
- $params = array($protect_description,$cascade ? 'cascade' : '');
- $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason), $params );
+ if ( $protect ) {
+ $params = array( $protect_description, $cascade ? 'cascade' : '' );
+ $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason ), $params );
} else {
$log->addEntry( 'unprotect', $this->mTitle, $reason );
}
@@ -2124,13 +2445,13 @@ class Article {
* @return String
*/
protected static function flattenRestrictions( $limit ) {
- if( !is_array( $limit ) ) {
+ if ( !is_array( $limit ) ) {
throw new MWException( 'Article::flattenRestrictions given non-array restriction set' );
}
$bits = array();
ksort( $limit );
- foreach( $limit as $action => $restrictions ) {
- if( $restrictions != '' ) {
+ foreach ( $limit as $action => $restrictions ) {
+ if ( $restrictions != '' ) {
$bits[] = "$action=$restrictions";
}
}
@@ -2146,7 +2467,7 @@ class Article {
$dbw = wfGetDB( DB_MASTER );
// Get the last revision
$rev = Revision::newFromTitle( $this->mTitle );
- if( is_null( $rev ) )
+ if ( is_null( $rev ) )
return false;
// Get the article's contents
@@ -2154,9 +2475,9 @@ class Article {
$blank = false;
// If the page is blank, use the text from the previous revision,
// which can only be blank if there's a move/import/protect dummy revision involved
- if( $contents == '' ) {
+ if ( $contents == '' ) {
$prev = $rev->getPrevious();
- if( $prev ) {
+ if ( $prev ) {
$contents = $prev->getText();
$blank = true;
}
@@ -2164,23 +2485,21 @@ class Article {
// Find out if there was only one contributor
// Only scan the last 20 revisions
- $limit = 20;
$res = $dbw->select( 'revision', 'rev_user_text',
- array( 'rev_page' => $this->getID() ), __METHOD__,
- array( 'LIMIT' => $limit )
+ array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
+ __METHOD__,
+ array( 'LIMIT' => 20 )
);
- if( $res === false )
+ if ( $res === false )
// This page has no revisions, which is very weird
return false;
- if( $res->numRows() > 1 )
- $hasHistory = true;
- else
- $hasHistory = false;
+
+ $hasHistory = ( $res->numRows() > 1 );
$row = $dbw->fetchObject( $res );
$onlyAuthor = $row->rev_user_text;
// Try to find a second contributor
- foreach( $res as $row ) {
- if( $row->rev_user_text != $onlyAuthor ) {
+ foreach ( $res as $row ) {
+ if ( $row->rev_user_text != $onlyAuthor ) {
$onlyAuthor = false;
break;
}
@@ -2188,18 +2507,18 @@ class Article {
$dbw->freeResult( $res );
// Generate the summary with a '$1' placeholder
- if( $blank ) {
+ if ( $blank ) {
// The current revision is blank and the one before is also
// blank. It's just not our lucky day
$reason = wfMsgForContent( 'exbeforeblank', '$1' );
} else {
- if( $onlyAuthor )
+ if ( $onlyAuthor )
$reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
else
$reason = wfMsgForContent( 'excontent', '$1' );
}
-
- if( $reason == '-' ) {
+
+ if ( $reason == '-' ) {
// Allow these UI messages to be blanked out cleanly
return '';
}
@@ -2208,7 +2527,7 @@ class Article {
$contents = preg_replace( "/[\n\r]/", ' ', $contents );
// Calculate the maximum amount of chars to get
// Max content length = max comment length - length of the comment (excl. $1) - '...'
- $maxLength = 255 - (strlen( $reason ) - 2) - 3;
+ $maxLength = 255 - ( strlen( $reason ) - 2 ) - 3;
$contents = $wgContLang->truncate( $contents, $maxLength );
// Remove possible unfinished links
$contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
@@ -2232,10 +2551,10 @@ class Article {
$reason = $this->DeleteReasonList;
- if( $reason != 'other' && $this->DeleteReason != '' ) {
+ if ( $reason != 'other' && $this->DeleteReason != '' ) {
// Entry from drop down menu + additional comment
$reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason;
- } elseif( $reason == 'other' ) {
+ } elseif ( $reason == 'other' ) {
$reason = $this->DeleteReason;
}
# Flag to hide all contents of the archived revisions
@@ -2244,7 +2563,7 @@ class Article {
# This code desperately needs to be totally rewritten
# Read-only check...
- if( wfReadOnly() ) {
+ if ( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
@@ -2252,7 +2571,7 @@ class Article {
# Check permissions
$permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
- if( count( $permission_errors ) > 0 ) {
+ if ( count( $permission_errors ) > 0 ) {
$wgOut->showPermissionsErrorPage( $permission_errors );
return;
}
@@ -2263,27 +2582,37 @@ class Article {
$dbw = wfGetDB( DB_MASTER );
$conds = $this->mTitle->pageCond();
$latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
- if( $latest === false ) {
- $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) );
+ if ( $latest === false ) {
+ $wgOut->showFatalError(
+ Html::rawElement(
+ 'div',
+ array( 'class' => 'error mw-error-cannotdelete' ),
+ wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
+ )
+ );
$wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
- LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() );
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'delete',
+ $this->mTitle->getPrefixedText()
+ );
return;
}
# Hack for big sites
$bigHistory = $this->isBigDeletion();
- if( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
+ if ( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
global $wgLang, $wgDeleteRevisionsLimit;
$wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
return;
}
- if( $confirm ) {
+ if ( $confirm ) {
$this->doDelete( $reason, $suppress );
- if( $wgRequest->getCheck( 'wpWatch' ) ) {
+ if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) {
$this->doWatch();
- } elseif( $this->mTitle->userIsWatching() ) {
+ } elseif ( $this->mTitle->userIsWatching() ) {
$this->doUnwatch();
}
return;
@@ -2291,14 +2620,20 @@ class Article {
// Generate deletion reason
$hasHistory = false;
- if( !$reason ) $reason = $this->generateReason($hasHistory);
+ if ( !$reason ) $reason = $this->generateReason( $hasHistory );
// If the page has a history, insert a warning
- if( $hasHistory && !$confirm ) {
+ if ( $hasHistory && !$confirm ) {
+ global $wgLang;
$skin = $wgUser->getSkin();
- $wgOut->addHTML( '<strong>' . wfMsgExt( 'historywarning', array( 'parseinline' ) ) . ' ' . $skin->historyLink() . '</strong>' );
- if( $bigHistory ) {
- global $wgLang, $wgDeleteRevisionsLimit;
+ $revisions = $this->estimateRevisionCount();
+ $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' .
+ wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) .
+ wfMsgHtml( 'word-separator' ) . $skin->historyLink() .
+ '</strong>'
+ );
+ if ( $bigHistory ) {
+ global $wgDeleteRevisionsLimit;
$wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
}
@@ -2312,7 +2647,7 @@ class Article {
*/
public function isBigDeletion() {
global $wgDeleteRevisionsLimit;
- if( $wgDeleteRevisionsLimit ) {
+ if ( $wgDeleteRevisionsLimit ) {
$revCount = $this->estimateRevisionCount();
return $revCount > $wgDeleteRevisionsLimit;
}
@@ -2325,10 +2660,10 @@ class Article {
public function estimateRevisionCount() {
$dbr = wfGetDB( DB_SLAVE );
// For an exact count...
- //return $dbr->selectField( 'revision', 'COUNT(*)',
+ // return $dbr->selectField( 'revision', 'COUNT(*)',
// array( 'rev_page' => $this->getId() ), __METHOD__ );
return $dbr->estimateRowCount( 'revision', '*',
- array( 'rev_page' => $this->getId() ), __METHOD__ );
+ array( 'rev_page' => $this->getId() ), __METHOD__ );
}
/**
@@ -2355,12 +2690,12 @@ class Article {
'LIMIT' => $num
) )
);
- if( !$res ) {
+ if ( !$res ) {
wfProfileOut( __METHOD__ );
return array();
}
$row = $db->fetchObject( $res );
- if( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
+ if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
$db = wfGetDB( DB_MASTER );
$continue--;
} else {
@@ -2385,24 +2720,33 @@ class Article {
wfDebug( "Article::confirmDelete\n" );
- $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->mTitle ) ) );
+ $deleteBackLink = $wgUser->getSkin()->link(
+ $this->mTitle,
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
+ $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addWikiMsg( 'confirmdeletetext' );
- if( $wgUser->isAllowed( 'suppressrevision' ) ) {
+ wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) );
+
+ if ( $wgUser->isAllowed( 'suppressrevision' ) ) {
$suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\">
<td></td>
- <td class='mw-input'>" .
+ <td class='mw-input'><strong>" .
Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
- "</td>
+ "</strong></td>
</tr>";
} else {
$suppress = '';
}
$checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching();
- $form = Xml::openElement( 'form', array( 'method' => 'post',
+ $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' ) ) ) .
@@ -2422,17 +2766,27 @@ class Article {
Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'wpReason', 60, $reason, array( 'type' => 'text', 'maxlength' => '255',
- 'tabindex' => '2', 'id' => 'wpReason' ) ) .
+ Html::input( 'wpReason', $reason, 'text', array(
+ 'size' => '60',
+ 'maxlength' => '255',
+ 'tabindex' => '2',
+ 'id' => 'wpReason',
+ 'autofocus'
+ ) ) .
"</td>
- </tr>
+ </tr>";
+ # Dissalow watching is user is not logged in
+ if ( $wgUser->isLoggedIn() ) {
+ $form .= "
<tr>
<td></td>
<td class='mw-input'>" .
Xml::checkLabel( wfMsg( 'watchthis' ),
'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
"</td>
- </tr>
+ </tr>";
+ }
+ $form .= "
$suppress
<tr>
<td></td>
@@ -2446,14 +2800,25 @@ class Article {
Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
Xml::closeElement( 'form' );
- if( $wgUser->isAllowed( 'editinterface' ) ) {
+ if ( $wgUser->isAllowed( 'editinterface' ) ) {
$skin = $wgUser->getSkin();
- $link = $skin->makeLink ( 'MediaWiki:Deletereason-dropdown', wfMsgHtml( 'delete-edit-reasonlist' ) );
+ $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' );
+ $link = $skin->link(
+ $title,
+ wfMsgHtml( 'delete-edit-reasonlist' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
$form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
}
$wgOut->addHTML( $form );
- LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() );
+ $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'delete',
+ $this->mTitle->getPrefixedText()
+ );
}
/**
@@ -2464,8 +2829,8 @@ class Article {
$id = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
$error = '';
- if( wfRunHooks('ArticleDelete', array(&$this, &$wgUser, &$reason, &$error)) ) {
- if( $this->doDeleteArticle( $reason, $suppress, $id ) ) {
+ if ( wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) {
+ if ( $this->doDeleteArticle( $reason, $suppress, $id ) ) {
$deleted = $this->mTitle->getPrefixedText();
$wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
@@ -2475,15 +2840,25 @@ class Article {
$wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
$wgOut->returnToMain( false );
- wfRunHooks('ArticleDeleteComplete', array(&$this, &$wgUser, $reason, $id));
+ wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) );
+ }
+ } else {
+ if ( $error == '' ) {
+ $wgOut->showFatalError(
+ Html::rawElement(
+ 'div',
+ array( 'class' => 'error mw-error-cannotdelete' ),
+ wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
+ )
+ );
+ $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'delete',
+ $this->mTitle->getPrefixedText()
+ );
} else {
- if( $error == '' ) {
- $wgOut->showFatalError( wfMsgExt( 'cannotdelete', array( 'parse' ) ) );
- $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
- LogEventsList::showLogExtract( $wgOut, 'delete', $this->mTitle->getPrefixedText() );
- } else {
- $wgOut->showFatalError( $error );
- }
+ $wgOut->showFatalError( $error );
}
}
}
@@ -2497,22 +2872,22 @@ class Article {
global $wgUseSquid, $wgDeferredUpdateList;
global $wgUseTrackbacks;
- wfDebug( __METHOD__."\n" );
+ wfDebug( __METHOD__ . "\n" );
$dbw = wfGetDB( DB_MASTER );
$ns = $this->mTitle->getNamespace();
$t = $this->mTitle->getDBkey();
$id = $id ? $id : $this->mTitle->getArticleID( GAID_FOR_UPDATE );
- if( $t == '' || $id == 0 ) {
+ if ( $t == '' || $id == 0 ) {
return false;
}
- $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getRawText() ), -1 );
+ $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable( $this->getRawText() ), -1 );
array_push( $wgDeferredUpdateList, $u );
// Bitfields to further suppress the content
- if( $suppress ) {
+ if ( $suppress ) {
$bitfield = 0;
// This should be 15...
$bitfield |= Revision::DELETED_TEXT;
@@ -2560,26 +2935,26 @@ class Article {
$dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
# Now that it's safely backed up, delete it
- $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__);
+ $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
$ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
- if( !$ok ) {
+ if ( !$ok ) {
$dbw->rollback();
return false;
}
-
+
# Fix category table counts
$cats = array();
$res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
- foreach( $res as $row ) {
- $cats []= $row->cl_to;
+ foreach ( $res as $row ) {
+ $cats [] = $row->cl_to;
}
$this->updateCategoryCounts( array(), $cats );
# If using cascading deletes, we can skip some explicit deletes
- if( !$dbw->cascadingDeletes() ) {
+ if ( !$dbw->cascadingDeletes() ) {
$dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
- if($wgUseTrackbacks)
+ if ( $wgUseTrackbacks )
$dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
# Delete outgoing links
@@ -2593,15 +2968,15 @@ class Article {
}
# If using cleanup triggers, we can skip some manual deletes
- if( !$dbw->cleanupTriggers() ) {
+ if ( !$dbw->cleanupTriggers() ) {
# Clean up recentchanges entries...
$dbw->delete( 'recentchanges',
- array( 'rc_type != '.RC_LOG,
+ array( 'rc_type != ' . RC_LOG,
'rc_namespace' => $this->mTitle->getNamespace(),
- 'rc_title' => $this->mTitle->getDBKey() ),
+ 'rc_title' => $this->mTitle->getDBkey() ),
__METHOD__ );
$dbw->delete( 'recentchanges',
- array( 'rc_type != '.RC_LOG, 'rc_cur_id' => $id ),
+ array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
__METHOD__ );
}
@@ -2653,17 +3028,17 @@ class Article {
$rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser );
$errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
- if( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
+ if ( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
$errors[] = array( 'sessionfailure' );
- if( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) {
+ if ( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) {
$errors[] = array( 'actionthrottledtext' );
}
# If there were errors, bail out now
- if( !empty( $errors ) )
+ if ( !empty( $errors ) )
return $errors;
- return $this->commitRollback($fromP, $summary, $bot, $resultDetails);
+ return $this->commitRollback( $fromP, $summary, $bot, $resultDetails );
}
/**
@@ -2675,95 +3050,102 @@ class Article {
* ly if you want to use custom permissions checks. If you don't, use
* doRollback() instead.
*/
- public function commitRollback($fromP, $summary, $bot, &$resultDetails) {
+ public function commitRollback( $fromP, $summary, $bot, &$resultDetails ) {
global $wgUseRCPatrol, $wgUser, $wgLang;
$dbw = wfGetDB( DB_MASTER );
- if( wfReadOnly() ) {
+ if ( wfReadOnly() ) {
return array( array( 'readonlytext' ) );
}
# Get the last editor
$current = Revision::newFromTitle( $this->mTitle );
- if( is_null( $current ) ) {
+ if ( is_null( $current ) ) {
# Something wrong... no page?
- return array(array('notanarticle'));
+ return array( array( 'notanarticle' ) );
}
$from = str_replace( '_', ' ', $fromP );
- if( $from != $current->getUserText() ) {
+ # User name given should match up with the top revision.
+ # If the user was deleted then $from should be empty.
+ if ( $from != $current->getUserText() ) {
$resultDetails = array( 'current' => $current );
- return array(array('alreadyrolled',
- htmlspecialchars($this->mTitle->getPrefixedText()),
- htmlspecialchars($fromP),
- htmlspecialchars($current->getUserText())
- ));
+ return array( array( 'alreadyrolled',
+ htmlspecialchars( $this->mTitle->getPrefixedText() ),
+ htmlspecialchars( $fromP ),
+ htmlspecialchars( $current->getUserText() )
+ ) );
}
- # Get the last edit not by this guy
- $user = intval( $current->getUser() );
- $user_text = $dbw->addQuotes( $current->getUserText() );
+ # Get the last edit not by this guy...
+ # Note: these may not be public values
+ $user = intval( $current->getRawUser() );
+ $user_text = $dbw->addQuotes( $current->getRawUserText() );
$s = $dbw->selectRow( 'revision',
array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
- array( 'rev_page' => $current->getPage(),
+ array( 'rev_page' => $current->getPage(),
"rev_user != {$user} OR rev_user_text != {$user_text}"
), __METHOD__,
- array( 'USE INDEX' => 'page_timestamp',
+ array( 'USE INDEX' => 'page_timestamp',
'ORDER BY' => 'rev_timestamp DESC' )
);
- if( $s === false ) {
+ 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 ) {
+ 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'));
+ return array( array( 'notvisiblerev' ) );
}
$set = array();
- if( $bot && $wgUser->isAllowed('markbotedits') ) {
+ if ( $bot && $wgUser->isAllowed( 'markbotedits' ) ) {
# Mark all reverted edits as bot
$set['rc_bot'] = 1;
}
- if( $wgUseRCPatrol ) {
+ if ( $wgUseRCPatrol ) {
# Mark all reverted edits as patrolled
$set['rc_patrolled'] = 1;
}
- if( $set ) {
+ if ( count( $set ) ) {
$dbw->update( 'recentchanges', $set,
- array( /* WHERE */
- 'rc_cur_id' => $current->getPage(),
- 'rc_user_text' => $current->getUserText(),
- "rc_timestamp > '{$s->rev_timestamp}'",
- ), __METHOD__
- );
+ array( /* WHERE */
+ 'rc_cur_id' => $current->getPage(),
+ 'rc_user_text' => $current->getUserText(),
+ "rc_timestamp > '{$s->rev_timestamp}'",
+ ), __METHOD__
+ );
}
# Generate the edit summary if necessary
$target = Revision::newFromId( $s->rev_id );
- if( empty( $summary ) ){
- $summary = wfMsgForContent( 'revertpage' );
+ if ( empty( $summary ) ) {
+ if ( $from == '' ) { // no public user name
+ $summary = wfMsgForContent( 'revertpage-nouser' );
+ } else {
+ $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())
+ $wgLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ), true ),
+ $current->getId(), $wgLang->timeanddate( $current->getTimestamp() )
);
$summary = wfMsgReplaceArgs( $summary, $args );
# Save
$flags = EDIT_UPDATE;
- if( $wgUser->isAllowed('minoredit') )
+ if ( $wgUser->isAllowed( 'minoredit' ) )
$flags |= EDIT_MINOR;
- if( $bot && ($wgUser->isAllowed('markbotedits') || $wgUser->isAllowed('bot')) )
+ if ( $bot && ( $wgUser->isAllowed( 'markbotedits' ) || $wgUser->isAllowed( 'bot' ) ) )
$flags |= EDIT_FORCE_BOT;
# Actually store the edit
$status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
- if( !empty( $status->value['revision'] ) ) {
+ if ( !empty( $status->value['revision'] ) ) {
$revId = $status->value['revision']->getId();
} else {
$revId = false;
@@ -2774,8 +3156,8 @@ class Article {
$resultDetails = array(
'summary' => $summary,
'current' => $current,
- 'target' => $target,
- 'newid' => $revId
+ 'target' => $target,
+ 'newid' => $revId
);
return array();
}
@@ -2795,19 +3177,19 @@ class Article {
$details
);
- if( in_array( array( 'actionthrottledtext' ), $result ) ) {
+ if ( in_array( array( 'actionthrottledtext' ), $result ) ) {
$wgOut->rateLimited();
return;
}
- if( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) {
+ if ( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) {
$wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
$errArray = $result[0];
$errMsg = array_shift( $errArray );
$wgOut->addWikiMsgArray( $errMsg, $errArray );
- if( isset( $details['current'] ) ){
+ if ( isset( $details['current'] ) ) {
$current = $details['current'];
- if( $current->getComment() != '' ) {
- $wgOut->addWikiMsgArray( 'editcomment', array(
+ if ( $current->getComment() != '' ) {
+ $wgOut->addWikiMsgArray( 'editcomment', array(
$wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
}
}
@@ -2816,19 +3198,19 @@ class Article {
# Display permissions errors before read-only message -- there's no
# point in misleading the user into thinking the inability to rollback
# is only temporary.
- if( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
+ if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
# array_diff is completely broken for arrays of arrays, sigh. Re-
# move any 'readonlytext' error manually.
$out = array();
- foreach( $result as $error ) {
- if( $error != array( 'readonlytext' ) ) {
- $out []= $error;
+ foreach ( $result as $error ) {
+ if ( $error != array( 'readonlytext' ) ) {
+ $out [] = $error;
}
}
$wgOut->showPermissionsErrorPage( $out );
return;
}
- if( $result == array( array( 'readonlytext' ) ) ) {
+ if ( $result == array( array( 'readonlytext' ) ) ) {
$wgOut->readOnlyPage();
return;
}
@@ -2838,14 +3220,18 @@ class Article {
$newId = $details['newid'];
$wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
- . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
+ if ( $current->getUserText() === '' ) {
+ $old = wfMsg( 'rev-deleted-user' );
+ } else {
+ $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
+ . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
+ }
$new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
. $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
$wgOut->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
$wgOut->returnToMain( false, $this->mTitle );
- if( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) {
+ if ( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) {
$de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true );
$de->showDiff( '', '' );
}
@@ -2857,8 +3243,11 @@ class Article {
*/
public function viewUpdates() {
global $wgDeferredUpdateList, $wgDisableCounters, $wgUser;
+ if ( wfReadOnly() ) {
+ return;
+ }
# Don't update page view counters on views from bot users (bug 14044)
- if( !$wgDisableCounters && !$wgUser->isAllowed('bot') && $this->getID() ) {
+ if ( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) && $this->getID() ) {
Article::incViewCount( $this->getID() );
$u = new SiteStatsUpdate( 1, 0, 0 );
array_push( $wgDeferredUpdateList, $u );
@@ -2871,8 +3260,8 @@ class Article {
* Prepare text which is about to be saved.
* Returns a stdclass with source, pst and output members
*/
- public function prepareTextForEdit( $text, $revid=null ) {
- if( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid) {
+ public function prepareTextForEdit( $text, $revid = null ) {
+ if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid ) {
// Already prepared
return $this->mPreparedEdit;
}
@@ -2881,9 +3270,7 @@ class Article {
$edit->revid = $revid;
$edit->newText = $text;
$edit->pst = $this->preSaveTransform( $text );
- $options = new ParserOptions;
- $options->setTidy( true );
- $options->enableLimitReport();
+ $options = $this->getParserOptions();
$edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid );
$edit->oldText = $this->getContent();
$this->mPreparedEdit = $edit;
@@ -2905,13 +3292,13 @@ class Article {
* @param $changed Whether or not the content actually changed
*/
public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
- global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgParser, $wgEnableParserCache;
+ global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgEnableParserCache;
wfProfileIn( __METHOD__ );
# Parse the text
# Be careful not to double-PST: $text is usually already PST-ed once
- if( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
+ if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
$editInfo = $this->prepareTextForEdit( $text, $newid );
} else {
@@ -2920,10 +3307,8 @@ class Article {
}
# Save it to the parser cache
- if( $wgEnableParserCache ) {
- $popts = new ParserOptions;
- $popts->setTidy( true );
- $popts->enableLimitReport();
+ if ( $wgEnableParserCache ) {
+ $popts = $this->getParserOptions();
$parserCache = ParserCache::singleton();
$parserCache->save( $editInfo->output, $this, $popts );
}
@@ -2931,11 +3316,11 @@ class Article {
# Update the links tables
$u = new LinksUpdate( $this->mTitle, $editInfo->output );
$u->doUpdate();
-
+
wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) );
- if( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
- if( 0 == mt_rand( 0, 99 ) ) {
+ if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
+ if ( 0 == mt_rand( 0, 99 ) ) {
// Flush old entries from the `recentchanges` table; we do this on
// random requests so as to avoid an increase in writes for no good reason
global $wgRCMaxAge;
@@ -2951,7 +3336,7 @@ class Article {
$title = $this->mTitle->getPrefixedDBkey();
$shortTitle = $this->mTitle->getDBkey();
- if( 0 == $id ) {
+ if ( 0 == $id ) {
wfProfileOut( __METHOD__ );
return;
}
@@ -2965,24 +3350,24 @@ class Article {
# Don't do this if $changed = false otherwise some idiot can null-edit a
# load of user talk pages and piss people off, nor if it's a minor edit
# by a properly-flagged bot.
- if( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed
+ if ( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed
&& !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) ) ) {
- if( wfRunHooks('ArticleEditUpdateNewTalk', array( &$this ) ) ) {
+ if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
$other = User::newFromName( $shortTitle, false );
- if( !$other ) {
- wfDebug( __METHOD__.": invalid username\n" );
- } elseif( User::isIP( $shortTitle ) ) {
+ if ( !$other ) {
+ wfDebug( __METHOD__ . ": invalid username\n" );
+ } elseif ( User::isIP( $shortTitle ) ) {
// An anonymous user
$other->setNewtalk( true );
- } elseif( $other->isLoggedIn() ) {
+ } elseif ( $other->isLoggedIn() ) {
$other->setNewtalk( true );
} else {
- wfDebug( __METHOD__. ": don't need to notify a nonexistent user\n" );
+ wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
}
}
}
- if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
$wgMessageCache->replace( $shortTitle, $text );
}
@@ -3016,56 +3401,112 @@ class Article {
public function setOldSubtitle( $oldid = 0 ) {
global $wgLang, $wgOut, $wgUser, $wgRequest;
- if( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
+ if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
return;
}
+ $unhide = $wgRequest->getInt( 'unhide' ) == 1 &&
+ $wgUser->matchEditToken( $wgRequest->getVal( 'token' ), $oldid );
+ # Cascade unhide param in links for easy deletion browsing
+ $extraParams = array();
+ if ( $wgRequest->getVal( 'unhide' ) ) {
+ $extraParams['unhide'] = 1;
+ }
$revision = Revision::newFromId( $oldid );
$current = ( $oldid == $this->mLatest );
$td = $wgLang->timeanddate( $this->mTimestamp, true );
+ $tddate = $wgLang->date( $this->mTimestamp, true );
+ $tdtime = $wgLang->time( $this->mTimestamp, true );
$sk = $wgUser->getSkin();
$lnk = $current
? wfMsgHtml( 'currentrevisionlink' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'currentrevisionlink' ) );
+ : $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'currentrevisionlink' ),
+ array(),
+ $extraParams,
+ array( 'known', 'noclasses' )
+ );
$curdiff = $current
? wfMsgHtml( 'diff' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=cur&oldid='.$oldid );
+ : $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'diff' ),
+ array(),
+ array(
+ 'diff' => 'cur',
+ 'oldid' => $oldid
+ ) + $extraParams,
+ array( 'known', 'noclasses' )
+ );
$prev = $this->mTitle->getPreviousRevisionID( $oldid ) ;
$prevlink = $prev
- ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousrevision' ), 'direction=prev&oldid='.$oldid )
+ ? $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'previousrevision' ),
+ array(),
+ array(
+ 'direction' => 'prev',
+ 'oldid' => $oldid
+ ) + $extraParams,
+ array( 'known', 'noclasses' )
+ )
: wfMsgHtml( 'previousrevision' );
$prevdiff = $prev
- ? $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=prev&oldid='.$oldid )
+ ? $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'diff' ),
+ array(),
+ array(
+ 'diff' => 'prev',
+ 'oldid' => $oldid
+ ) + $extraParams,
+ array( 'known', 'noclasses' )
+ )
: wfMsgHtml( 'diff' );
$nextlink = $current
? wfMsgHtml( 'nextrevision' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextrevision' ), 'direction=next&oldid='.$oldid );
+ : $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'nextrevision' ),
+ array(),
+ array(
+ 'direction' => 'next',
+ 'oldid' => $oldid
+ ) + $extraParams,
+ array( 'known', 'noclasses' )
+ );
$nextdiff = $current
? wfMsgHtml( 'diff' )
- : $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'diff' ), 'diff=next&oldid='.$oldid );
-
- $cdel='';
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
- $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
- if( $revision->isCurrent() ) {
- // We don't handle top deleted edits too well
- $cdel = wfMsgHtml( 'rev-delundel' );
- } else if( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $cdel = wfMsgHtml( 'rev-delundel' );
+ : $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'diff' ),
+ array(),
+ array(
+ 'diff' => 'next',
+ 'oldid' => $oldid
+ ) + $extraParams,
+ array( 'known', 'noclasses' )
+ );
+
+ $cdel = '';
+ // User can delete revisions or view deleted revisions...
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if ( $canHide || ( $revision->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) ) {
+ if ( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $cdel = $sk->revDeleteLinkDisabled( $canHide ); // rev was hidden from Sysops
} 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>";
+ $query = array(
+ 'type' => 'revision',
+ 'target' => $this->mTitle->getPrefixedDbkey(),
+ 'ids' => $oldid
+ );
+ $cdel = $sk->revDeleteLink( $query, $revision->isDeleted( File::DELETED_RESTRICTED ), $canHide );
}
- $cdel = "(<small>$cdel</small>) ";
+ $cdel .= ' ';
}
- $unhide = $wgRequest->getInt('unhide') == 1 && $wgUser->matchEditToken( $wgRequest->getVal('token'), $oldid );
+
# Show user links if allowed to see them. If hidden, then show them only if requested...
$userlinks = $sk->revUserTools( $revision, !$unhide );
@@ -3074,11 +3515,20 @@ class Article {
? 'revision-info-current'
: 'revision-info';
- $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsgExt( $infomsg, array( 'parseinline', 'replaceafter' ),
- $td, $userlinks, $revision->getID() ) . "</div>\n" .
-
- "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ),
- $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
+ $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" .
+ wfMsgExt(
+ $infomsg,
+ array( 'parseinline', 'replaceafter' ),
+ $td,
+ $userlinks,
+ $revision->getID(),
+ $tddate,
+ $tdtime,
+ $revision->getUser()
+ ) .
+ "</div>\n" .
+ "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ),
+ $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
$wgOut->setSubtitle( $r );
}
@@ -3102,20 +3552,20 @@ class Article {
*/
protected function tryFileCache() {
static $called = false;
- if( $called ) {
+ if ( $called ) {
wfDebug( "Article::tryFileCache(): called twice!?\n" );
return false;
}
$called = true;
- if( $this->isFileCacheable() ) {
+ if ( $this->isFileCacheable() ) {
$cache = new HTMLFileCache( $this->mTitle );
- if( $cache->isFileCacheGood( $this->mTouched ) ) {
+ if ( $cache->isFileCacheGood( $this->mTouched ) ) {
wfDebug( "Article::tryFileCache(): about to load file\n" );
$cache->loadFromFileCache();
return true;
} else {
wfDebug( "Article::tryFileCache(): starting buffer\n" );
- ob_start( array(&$cache, 'saveToFileCache' ) );
+ ob_start( array( &$cache, 'saveToFileCache' ) );
}
} else {
wfDebug( "Article::tryFileCache(): not cacheable\n" );
@@ -3129,10 +3579,10 @@ class Article {
*/
public function isFileCacheable() {
$cacheable = false;
- if( HTMLFileCache::useFileCache() ) {
+ if ( HTMLFileCache::useFileCache() ) {
$cacheable = $this->getID() && !$this->mRedirectedFrom;
// Extension may have reason to disable file caching on some pages.
- if( $cacheable ) {
+ if ( $cacheable ) {
$cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) );
}
}
@@ -3144,7 +3594,7 @@ class Article {
*
*/
public function checkTouched() {
- if( !$this->mDataLoaded ) {
+ if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return !$this->mIsRedirect;
@@ -3155,7 +3605,7 @@ class Article {
*/
public function getTouched() {
# Ensure that page data has been loaded
- if( !$this->mDataLoaded ) {
+ if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return $this->mTouched;
@@ -3165,7 +3615,7 @@ class Article {
* Get the page_latest field
*/
public function getLatest() {
- if( !$this->mDataLoaded ) {
+ if ( !$this->mDataLoaded ) {
$this->loadPageData();
}
return (int)$this->mLatest;
@@ -3193,7 +3643,7 @@ class Article {
$revision->insertOn( $dbw );
$this->updateRevisionOn( $dbw, $revision );
- wfRunHooks( 'NewRevisionFromEditComplete', array($this, $revision, false, $wgUser) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $wgUser ) );
wfProfileOut( __METHOD__ );
}
@@ -3205,14 +3655,15 @@ class Article {
*/
public static function incViewCount( $id ) {
$id = intval( $id );
- global $wgHitcounterUpdateFreq, $wgDBtype;
+ global $wgHitcounterUpdateFreq;
$dbw = wfGetDB( DB_MASTER );
$pageTable = $dbw->tableName( 'page' );
$hitcounterTable = $dbw->tableName( 'hitcounter' );
$acchitsTable = $dbw->tableName( 'acchits' );
+ $dbType = $dbw->getType();
- if( $wgHitcounterUpdateFreq <= 1 ) {
+ if ( $wgHitcounterUpdateFreq <= 1 || $dbType == 'sqlite' ) {
$dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" );
return;
}
@@ -3222,37 +3673,36 @@ class Article {
$dbw->query( "INSERT INTO $hitcounterTable (hc_id) VALUES ({$id})" );
- $checkfreq = intval( $wgHitcounterUpdateFreq/25 + 1 );
- if( (rand() % $checkfreq != 0) or ($dbw->lastErrno() != 0) ){
+ $checkfreq = intval( $wgHitcounterUpdateFreq / 25 + 1 );
+ if ( ( rand() % $checkfreq != 0 ) or ( $dbw->lastErrno() != 0 ) ) {
# Most of the time (or on SQL errors), skip row count check
$dbw->ignoreErrors( $oldignore );
return;
}
- $res = $dbw->query("SELECT COUNT(*) as n FROM $hitcounterTable");
+ $res = $dbw->query( "SELECT COUNT(*) as n FROM $hitcounterTable" );
$row = $dbw->fetchObject( $res );
$rown = intval( $row->n );
- if( $rown >= $wgHitcounterUpdateFreq ){
+ if ( $rown >= $wgHitcounterUpdateFreq ) {
wfProfileIn( 'Article::incViewCount-collect' );
$old_user_abort = ignore_user_abort( true );
- if($wgDBtype == 'mysql')
- $dbw->query("LOCK TABLES $hitcounterTable WRITE");
- $tabletype = $wgDBtype == 'mysql' ? "ENGINE=HEAP " : '';
- $dbw->query("CREATE TEMPORARY TABLE $acchitsTable $tabletype AS ".
- "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable ".
- 'GROUP BY hc_id');
- $dbw->query("DELETE FROM $hitcounterTable");
- if($wgDBtype == 'mysql') {
- $dbw->query('UNLOCK TABLES');
- $dbw->query("UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n ".
- 'WHERE page_id = hc_id');
+ $dbw->lockTables( array(), array( 'hitcounter' ), __METHOD__, false );
+ $tabletype = $dbType == 'mysql' ? "ENGINE=HEAP " : '';
+ $dbw->query( "CREATE TEMPORARY TABLE $acchitsTable $tabletype AS " .
+ "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable " .
+ 'GROUP BY hc_id', __METHOD__ );
+ $dbw->delete( 'hitcounter', '*', __METHOD__ );
+ $dbw->unlockTables( __METHOD__ );
+ if ( $dbType == 'mysql' ) {
+ $dbw->query( "UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n " .
+ 'WHERE page_id = hc_id', __METHOD__ );
}
else {
- $dbw->query("UPDATE $pageTable SET page_counter=page_counter + hc_n ".
- "FROM $acchitsTable WHERE page_id = hc_id");
+ $dbw->query( "UPDATE $pageTable SET page_counter=page_counter + hc_n " .
+ "FROM $acchitsTable WHERE page_id = hc_id", __METHOD__ );
}
- $dbw->query("DROP TABLE $acchitsTable");
+ $dbw->query( "DROP TABLE $acchitsTable", __METHOD__ );
ignore_user_abort( $old_user_abort );
wfProfileOut( 'Article::incViewCount-collect' );
@@ -3271,10 +3721,9 @@ class Article {
*
* @param $title a title object
*/
-
public static function onArticleCreate( $title ) {
# Update existence markers on article/talk tabs...
- if( $title->isTalkPage() ) {
+ if ( $title->isTalkPage() ) {
$other = $title->getSubjectPage();
} else {
$other = $title->getTalkPage();
@@ -3290,7 +3739,7 @@ class Article {
public static function onArticleDelete( $title ) {
global $wgMessageCache;
# Update existence markers on article/talk tabs...
- if( $title->isTalkPage() ) {
+ if ( $title->isTalkPage() ) {
$other = $title->getSubjectPage();
} else {
$other = $title->getTalkPage();
@@ -3305,16 +3754,16 @@ class Article {
HTMLFileCache::clearFileCache( $title );
# Messages
- if( $title->getNamespace() == NS_MEDIAWIKI ) {
+ if ( $title->getNamespace() == NS_MEDIAWIKI ) {
$wgMessageCache->replace( $title->getDBkey(), false );
}
# Images
- if( $title->getNamespace() == NS_FILE ) {
+ if ( $title->getNamespace() == NS_FILE ) {
$update = new HTMLCacheUpdate( $title, 'imagelinks' );
$update->doUpdate();
}
# User talk pages
- if( $title->getNamespace() == NS_USER_TALK ) {
+ if ( $title->getNamespace() == NS_USER_TALK ) {
$user = User::newFromName( $title->getText(), false );
$user->setNewtalk( false );
}
@@ -3359,7 +3808,7 @@ class Article {
public function info() {
global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser;
- if( !$wgAllowPageInfo ) {
+ if ( !$wgAllowPageInfo ) {
$wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
return;
}
@@ -3370,9 +3819,9 @@ class Article {
$wgOut->setPageTitleActionText( wfMsg( 'info_short' ) );
$wgOut->setSubtitle( wfMsgHtml( 'infosubtitle' ) );
- if( !$this->mTitle->exists() ) {
+ if ( !$this->mTitle->exists() ) {
$wgOut->addHTML( '<div class="noarticletext">' );
- if( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+ if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
// This doesn't quite make sense; the user is asking for
// information about the _page_, not the message... -- RC
$wgOut->addHTML( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) );
@@ -3398,14 +3847,14 @@ class Article {
$pageInfo = $this->pageCountInfo( $page );
$talkInfo = $this->pageCountInfo( $page->getTalkPage() );
- $wgOut->addHTML( "<ul><li>" . wfMsg("numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' );
- $wgOut->addHTML( "<li>" . wfMsg('numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>');
- if( $talkInfo ) {
- $wgOut->addHTML( '<li>' . wfMsg("numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>');
+ $wgOut->addHTML( "<ul><li>" . wfMsg( "numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' );
+ $wgOut->addHTML( "<li>" . wfMsg( 'numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>' );
+ if ( $talkInfo ) {
+ $wgOut->addHTML( '<li>' . wfMsg( "numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>' );
}
- $wgOut->addHTML( '<li>' . wfMsg("numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' );
- if( $talkInfo ) {
- $wgOut->addHTML( '<li>' . wfMsg('numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' );
+ $wgOut->addHTML( '<li>' . wfMsg( "numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' );
+ if ( $talkInfo ) {
+ $wgOut->addHTML( '<li>' . wfMsg( 'numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' );
}
$wgOut->addHTML( '</ul>' );
}
@@ -3418,9 +3867,9 @@ class Article {
* @param $title Title object
* @return array
*/
- protected function pageCountInfo( $title ) {
+ public function pageCountInfo( $title ) {
$id = $title->getArticleId();
- if( $id == 0 ) {
+ if ( $id == 0 ) {
return false;
}
$dbr = wfGetDB( DB_SLAVE );
@@ -3451,7 +3900,7 @@ class Article {
public function getUsedTemplates() {
$result = array();
$id = $this->mTitle->getArticleID();
- if( $id == 0 ) {
+ if ( $id == 0 ) {
return array();
}
$dbr = wfGetDB( DB_SLAVE );
@@ -3459,8 +3908,8 @@ class Article {
array( 'tl_namespace', 'tl_title' ),
array( 'tl_from' => $id ),
__METHOD__ );
- if( $res !== false ) {
- foreach( $res as $row ) {
+ if ( $res !== false ) {
+ foreach ( $res as $row ) {
$result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title );
}
}
@@ -3477,17 +3926,17 @@ class Article {
public function getHiddenCategories() {
$result = array();
$id = $this->mTitle->getArticleID();
- if( $id == 0 ) {
+ if ( $id == 0 ) {
return array();
}
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
array( 'cl_to' ),
array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
- 'page_namespace' => NS_CATEGORY, 'page_title=cl_to'),
+ 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
__METHOD__ );
- if( $res !== false ) {
- foreach( $res as $row ) {
+ if ( $res !== false ) {
+ foreach ( $res as $row ) {
$result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
}
}
@@ -3508,24 +3957,24 @@ class Article {
# Redirect autosummaries
$ot = Title::newFromRedirect( $oldtext );
$rt = Title::newFromRedirect( $newtext );
- if( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
+ if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
}
# New page autosummaries
- if( $flags & EDIT_NEW && strlen( $newtext ) ) {
+ 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),
+ str_replace( "\n", ' ', $newtext ),
max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
return wfMsgForContent( 'autosumm-new', $truncatedtext );
}
# Blanking autosummaries
- if( $oldtext != '' && $newtext == '' ) {
+ if ( $oldtext != '' && $newtext == '' ) {
return wfMsgForContent( 'autosumm-blank' );
- } elseif( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500) {
+ } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
# Removing more than 90% of the article
global $wgContLang;
$truncatedtext = $wgContLang->truncate(
@@ -3547,72 +3996,108 @@ class Article {
* @param $text String
* @param $cache Boolean
*/
- public function outputWikiText( $text, $cache = true ) {
- global $wgParser, $wgUser, $wgOut, $wgEnableParserCache, $wgUseFileCache;
-
- $popts = $wgOut->parserOptions();
- $popts->setTidy(true);
- $popts->enableLimitReport();
- $parserOutput = $wgParser->parse( $text, $this->mTitle,
- $popts, true, true, $this->getRevIdFetched() );
- $popts->setTidy(false);
- $popts->enableLimitReport( false );
- if( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) {
+ public function outputWikiText( $text, $cache = true, $parserOptions = false ) {
+ global $wgOut;
+
+ $this->mParserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions );
+ $wgOut->addParserOutput( $this->mParserOutput );
+ }
+
+ /**
+ * This does all the heavy lifting for outputWikitext, except it returns the parser
+ * output instead of sending it straight to $wgOut. Makes things nice and simple for,
+ * say, embedding thread pages within a discussion system (LiquidThreads)
+ */
+ public function getOutputFromWikitext( $text, $cache = true, $parserOptions = false ) {
+ global $wgParser, $wgOut, $wgEnableParserCache, $wgUseFileCache;
+
+ if ( !$parserOptions ) {
+ $parserOptions = $this->getParserOptions();
+ }
+
+ $time = - wfTime();
+ $this->mParserOutput = $wgParser->parse( $text, $this->mTitle,
+ $parserOptions, true, true, $this->getRevIdFetched() );
+ $time += wfTime();
+
+ # Timing hack
+ if ( $time > 3 ) {
+ wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
+ $this->mTitle->getPrefixedDBkey() ) );
+ }
+
+ if ( $wgEnableParserCache && $cache && $this && $this->mParserOutput->getCacheTime() != -1 ) {
$parserCache = ParserCache::singleton();
- $parserCache->save( $parserOutput, $this, $popts );
+ $parserCache->save( $this->mParserOutput, $this, $parserOptions );
}
// Make sure file cache is not used on uncacheable content.
// Output that has magic words in it can still use the parser cache
// (if enabled), though it will generally expire sooner.
- if( $parserOutput->getCacheTime() == -1 || $parserOutput->containsOldMagic() ) {
+ if ( $this->mParserOutput->getCacheTime() == -1 || $this->mParserOutput->containsOldMagic() ) {
$wgUseFileCache = false;
}
+ $this->doCascadeProtectionUpdates( $this->mParserOutput );
+ return $this->mParserOutput;
+ }
- if( $this->isCurrent() && !wfReadOnly() && $this->mTitle->areRestrictionsCascading() ) {
- // templatelinks table may have become out of sync,
- // especially if using variable-based transclusions.
- // For paranoia, check if things have changed and if
- // so apply updates to the database. This will ensure
- // that cascaded protections apply as soon as the changes
- // are visible.
+ /**
+ * Get parser options suitable for rendering the primary article wikitext
+ */
+ public function getParserOptions() {
+ global $wgUser;
+ if ( !$this->mParserOptions ) {
+ $this->mParserOptions = new ParserOptions( $wgUser );
+ $this->mParserOptions->setTidy( true );
+ $this->mParserOptions->enableLimitReport();
+ }
+ return $this->mParserOptions;
+ }
- # Get templates from templatelinks
- $id = $this->mTitle->getArticleID();
+ protected function doCascadeProtectionUpdates( $parserOutput ) {
+ if ( !$this->isCurrent() || wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
+ return;
+ }
- $tlTemplates = array();
+ // templatelinks table may have become out of sync,
+ // especially if using variable-based transclusions.
+ // For paranoia, check if things have changed and if
+ // so apply updates to the database. This will ensure
+ // that cascaded protections apply as soon as the changes
+ // are visible.
- $dbr = wfGetDB( DB_SLAVE );
- $res = $dbr->select( array( 'templatelinks' ),
- array( 'tl_namespace', 'tl_title' ),
- array( 'tl_from' => $id ),
- __METHOD__ );
+ # Get templates from templatelinks
+ $id = $this->mTitle->getArticleID();
- global $wgContLang;
- foreach( $res as $row ) {
- $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true;
- }
+ $tlTemplates = array();
- # Get templates from parser output.
- $poTemplates = array();
- foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
- foreach ( $templates as $dbk => $id ) {
- $key = $row->tl_namespace . ':'. $row->tl_title;
- $poTemplates["$ns:$dbk"] = true;
- }
- }
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( array( 'templatelinks' ),
+ array( 'tl_namespace', 'tl_title' ),
+ array( 'tl_from' => $id ),
+ __METHOD__ );
- # Get the diff
- # Note that we simulate array_diff_key in PHP <5.0.x
- $templates_diff = array_diff_key( $poTemplates, $tlTemplates );
+ global $wgContLang;
+ foreach ( $res as $row ) {
+ $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true;
+ }
- if( count( $templates_diff ) > 0 ) {
- # Whee, link updates time.
- $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
- $u->doUpdate();
+ # Get templates from parser output.
+ $poTemplates = array();
+ foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
+ foreach ( $templates as $dbk => $id ) {
+ $poTemplates["$ns:$dbk"] = true;
}
}
- $wgOut->addParserOutput( $parserOutput );
+ # Get the diff
+ # Note that we simulate array_diff_key in PHP <5.0.x
+ $templates_diff = array_diff_key( $poTemplates, $tlTemplates );
+
+ if ( count( $templates_diff ) > 0 ) {
+ # Whee, link updates time.
+ $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
+ $u->doUpdate();
+ }
}
/**
@@ -3634,27 +4119,30 @@ class Article {
#
# Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE.
$insertCats = array_merge( $added, $deleted );
- if( !$insertCats ) {
+ if ( !$insertCats ) {
# Okay, nothing to do
return;
}
$insertRows = array();
- foreach( $insertCats as $cat ) {
- $insertRows[] = array( 'cat_title' => $cat );
+ foreach ( $insertCats as $cat ) {
+ $insertRows[] = array(
+ 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ),
+ '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 ) {
+ if ( $ns == NS_CATEGORY ) {
$addFields[] = 'cat_subcats = cat_subcats + 1';
$removeFields[] = 'cat_subcats = cat_subcats - 1';
- } elseif( $ns == NS_FILE ) {
+ } elseif ( $ns == NS_FILE ) {
$addFields[] = 'cat_files = cat_files + 1';
$removeFields[] = 'cat_files = cat_files - 1';
}
- if( $added ) {
+ if ( $added ) {
$dbw->update(
'category',
$addFields,
@@ -3662,7 +4150,7 @@ class Article {
__METHOD__
);
}
- if( $deleted ) {
+ if ( $deleted ) {
$dbw->update(
'category',
$removeFields,
@@ -3671,4 +4159,37 @@ class Article {
);
}
}
+
+ /** Lightweight method to get the parser output for a page, checking the parser cache
+ * and so on. Doesn't consider most of the stuff that Article::view is forced to
+ * consider, so it's not appropriate to use there.
+ */
+ function getParserOutput( $oldid = null ) {
+ global $wgEnableParserCache, $wgUser, $wgOut;
+
+ // Should the parser cache be used?
+ $useParserCache = $wgEnableParserCache &&
+ intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 &&
+ $this->exists() &&
+ $oldid === null;
+
+ wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
+ if ( $wgUser->getOption( 'stubthreshold' ) ) {
+ wfIncrStats( 'pcache_miss_stub' );
+ }
+
+ $parserOutput = false;
+ if ( $useParserCache ) {
+ $parserOutput = ParserCache::singleton()->get( $this, $this->getParserOptions() );
+ }
+
+ if ( $parserOutput === false ) {
+ // Cache miss; parse and output it.
+ $rev = Revision::newFromTitle( $this->getTitle(), $oldid );
+
+ return $this->getOutputFromWikitext( $rev->getText(), $useParserCache );
+ } else {
+ return $parserOutput;
+ }
+ }
}
diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php
index b29e13f2..87ac8adb 100644
--- a/includes/AuthPlugin.php
+++ b/includes/AuthPlugin.php
@@ -63,8 +63,9 @@ class AuthPlugin {
* Modify options in the login template.
*
* @param $template UserLoginTemplate object.
+ * @param $type String 'signup' or 'login'.
*/
- public function modifyUITemplate( &$template ) {
+ public function modifyUITemplate( &$template, &$type ) {
# Override this!
$template->set( 'usedomain', false );
}
@@ -97,7 +98,7 @@ class AuthPlugin {
* The User object is passed by reference so it can be modified; don't
* forget the & on your function declaration.
*
- * @param User $user
+ * @param $user User object
*/
public function updateUser( &$user ) {
# Override this and do something
@@ -116,13 +117,32 @@ class AuthPlugin {
*
* This is just a question, and shouldn't perform any actions.
*
- * @return bool
+ * @return Boolean
*/
public function autoCreate() {
return false;
}
/**
+ * Allow a property change? Properties are the same as preferences
+ * and use the same keys. 'Realname' 'Emailaddress' and 'Nickname'
+ * all reference this.
+ *
+ * @return Boolean
+ */
+ public function allowPropChange( $prop = '' ) {
+ if( $prop == 'realname' && is_callable( array( $this, 'allowRealNameChange' ) ) ) {
+ return $this->allowRealNameChange();
+ } elseif( $prop == 'emailaddress' && is_callable( array( $this, 'allowEmailChange' ) ) ) {
+ return $this->allowEmailChange();
+ } elseif( $prop == 'nickname' && is_callable( array( $this, 'allowNickChange' ) ) ) {
+ return $this->allowNickChange();
+ } else {
+ return true;
+ }
+ }
+
+ /**
* Can users change their passwords?
*
* @return bool
@@ -152,7 +172,7 @@ class AuthPlugin {
* Return true if successful.
*
* @param $user User object.
- * @return bool
+ * @return Boolean
*/
public function updateExternalDB( $user ) {
return true;
@@ -161,7 +181,7 @@ class AuthPlugin {
/**
* Check to see if external accounts can be created.
* Return true if external accounts can be created.
- * @return bool
+ * @return Boolean
*/
public function canCreateAccounts() {
return false;
@@ -171,11 +191,11 @@ class AuthPlugin {
* Add a user to the external authentication database.
* Return true if successful.
*
- * @param User $user - only the name should be assumed valid at this point
- * @param string $password
- * @param string $email
- * @param string $realname
- * @return bool
+ * @param $user User: only the name should be assumed valid at this point
+ * @param $password String
+ * @param $email String
+ * @param $realname String
+ * @return Boolean
*/
public function addUser( $user, $password, $email='', $realname='' ) {
return true;
@@ -188,7 +208,7 @@ class AuthPlugin {
*
* This is just a question, and shouldn't perform any actions.
*
- * @return bool
+ * @return Boolean
*/
public function strict() {
return false;
@@ -199,7 +219,7 @@ class AuthPlugin {
* If either this or strict() returns true, local authentication is not used.
*
* @param $username String: username.
- * @return bool
+ * @return Boolean
*/
public function strictUserAuth( $username ) {
return false;
@@ -214,7 +234,7 @@ class AuthPlugin {
* forget the & on your function declaration.
*
* @param $user User object.
- * @param $autocreate bool True if user is being autocreated on login
+ * @param $autocreate Boolean: True if user is being autocreated on login
*/
public function initUser( &$user, $autocreate=false ) {
# Override this to do something.
@@ -232,7 +252,6 @@ class AuthPlugin {
* Get an instance of a User object
*
* @param $user User
- * @public
*/
public function getUserInstance( User &$user ) {
return new AuthPluginUser( $user );
diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php
index 85e7e668..cecb53f9 100644
--- a/includes/AutoLoader.php
+++ b/includes/AutoLoader.php
@@ -1,9 +1,6 @@
<?php
-
/* This defines autoloading handler for whole MediaWiki framework */
-ini_set('unserialize_callback_func', '__autoload' );
-
# Locations of core classes
# Extension classes are specified with $wgAutoloadClasses
# This array is a global instead of a static member of AutoLoader to work around a bug in APC
@@ -27,11 +24,23 @@ $wgAutoloadLocalClasses = array(
'Categoryfinder' => 'includes/Categoryfinder.php',
'CategoryPage' => 'includes/CategoryPage.php',
'CategoryViewer' => 'includes/CategoryPage.php',
+ 'CdbFunctions' => 'includes/Cdb_PHP.php',
+ 'CdbReader' => 'includes/Cdb.php',
+ 'CdbReader_DBA' => 'includes/Cdb.php',
+ 'CdbReader_PHP' => 'includes/Cdb_PHP.php',
+ 'CdbWriter' => 'includes/Cdb.php',
+ 'CdbWriter_DBA' => 'includes/Cdb.php',
+ 'CdbWriter_PHP' => 'includes/Cdb_PHP.php',
'ChangesList' => 'includes/ChangesList.php',
'ChangesFeed' => 'includes/ChangesFeed.php',
'ChangeTags' => 'includes/ChangeTags.php',
'ChannelFeed' => 'includes/Feed.php',
+ 'Cookie' => 'includes/HttpFunctions.php',
+ 'CookieJar' => 'includes/HttpFunctions.php',
'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php',
+ 'ConfEditor' => 'includes/ConfEditor.php',
+ 'ConfEditorParseError' => 'includes/ConfEditor.php',
+ 'ConfEditorToken' => 'includes/ConfEditor.php',
'ConstantDependency' => 'includes/CacheDependency.php',
'CreativeCommonsRdf' => 'includes/Metadata.php',
'Credits' => 'includes/Credits.php',
@@ -66,33 +75,58 @@ $wgAutoloadLocalClasses = array(
'ExternalStoreDB' => 'includes/ExternalStoreDB.php',
'ExternalStoreHttp' => 'includes/ExternalStoreHttp.php',
'ExternalStore' => 'includes/ExternalStore.php',
+ 'ExternalUser' => 'includes/ExternalUser.php',
+ 'ExternalUser_Hardcoded' => 'includes/extauth/Hardcoded.php',
+ 'ExternalUser_MediaWiki' => 'includes/extauth/MediaWiki.php',
+ 'ExternalUser_vB' => 'includes/extauth/vB.php',
'FatalError' => 'includes/Exception.php',
'FakeTitle' => 'includes/FakeTitle.php',
+ 'FakeMemCachedClient' => 'includes/ObjectCache.php',
'FauxRequest' => 'includes/WebRequest.php',
+ 'FauxResponse' => 'includes/WebResponse.php',
'FeedItem' => 'includes/Feed.php',
'FeedUtils' => 'includes/FeedUtils.php',
'FileDeleteForm' => 'includes/FileDeleteForm.php',
'FileDependency' => 'includes/CacheDependency.php',
'FileRevertForm' => 'includes/FileRevertForm.php',
- 'FileStore' => 'includes/FileStore.php',
'ForkController' => 'includes/ForkController.php',
'FormatExif' => 'includes/Exif.php',
'FormOptions' => 'includes/FormOptions.php',
- 'FSException' => 'includes/FileStore.php',
- 'FSTransaction' => 'includes/FileStore.php',
+ 'GIFMetadataExtractor' => 'includes/media/GIFMetadataExtractor.php',
+ 'GIFHandler' => 'includes/media/GIF.php',
'GlobalDependency' => 'includes/CacheDependency.php',
'HashBagOStuff' => 'includes/BagOStuff.php',
'HashtableReplacer' => 'includes/StringUtils.php',
'HistoryBlobCurStub' => 'includes/HistoryBlob.php',
'HistoryBlob' => 'includes/HistoryBlob.php',
'HistoryBlobStub' => 'includes/HistoryBlob.php',
+ 'HistoryPage' => 'includes/HistoryPage.php',
+ 'HistoryPager' => 'includes/HistoryPage.php',
+ 'Html' => 'includes/Html.php',
'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php',
'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
'HTMLFileCache' => 'includes/HTMLFileCache.php',
+ 'HTMLForm' => 'includes/HTMLForm.php',
+ 'HTMLFormField' => 'includes/HTMLForm.php',
+ 'HTMLTextField' => 'includes/HTMLForm.php',
+ 'HTMLIntField' => 'includes/HTMLForm.php',
+ 'HTMLTextAreaField' => 'includes/HTMLForm.php',
+ 'HTMLFloatField' => 'includes/HTMLForm.php',
+ 'HTMLHiddenField' => 'includes/HTMLForm.php',
+ 'HTMLSubmitField' => 'includes/HTMLForm.php',
+ 'HTMLEditTools' => 'includes/HTMLForm.php',
+ 'HTMLCheckField' => 'includes/HTMLForm.php',
+ 'HTMLSelectField' => 'includes/HTMLForm.php',
+ 'HTMLSelectOrOtherField' => 'includes/HTMLForm.php',
+ 'HTMLMultiSelectField' => 'includes/HTMLForm.php',
+ 'HTMLRadioField' => 'includes/HTMLForm.php',
+ 'HTMLInfoField' => 'includes/HTMLForm.php',
'Http' => 'includes/HttpFunctions.php',
+ 'HttpRequest' => 'includes/HttpFunctions.php',
'IEContentAnalyzer' => 'includes/IEContentAnalyzer.php',
'ImageGallery' => 'includes/ImageGallery.php',
'ImageHistoryList' => 'includes/ImagePage.php',
+ 'ImageHistoryPseudoPager' => 'includes/ImagePage.php',
'ImagePage' => 'includes/ImagePage.php',
'ImageQueryPage' => 'includes/ImageQueryPage.php',
'IncludableSpecialPage' => 'includes/SpecialPage.php',
@@ -100,6 +134,10 @@ $wgAutoloadLocalClasses = array(
'Interwiki' => 'includes/Interwiki.php',
'IP' => 'includes/IP.php',
'Job' => 'includes/JobQueue.php',
+ 'JSMin' => 'includes/JSMin.php',
+ 'LCStore_DB' => 'includes/LocalisationCache.php',
+ 'LCStore_CDB' => 'includes/LocalisationCache.php',
+ 'LCStore_Null' => 'includes/LocalisationCache.php',
'License' => 'includes/Licenses.php',
'Licenses' => 'includes/Licenses.php',
'LinkBatch' => 'includes/LinkBatch.php',
@@ -107,6 +145,8 @@ $wgAutoloadLocalClasses = array(
'Linker' => 'includes/Linker.php',
'LinkFilter' => 'includes/LinkFilter.php',
'LinksUpdate' => 'includes/LinksUpdate.php',
+ 'LocalisationCache' => 'includes/LocalisationCache.php',
+ 'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php',
'LogPage' => 'includes/LogPage.php',
'LogPager' => 'includes/LogEventsList.php',
'LogEventsList' => 'includes/LogEventsList.php',
@@ -122,24 +162,24 @@ $wgAutoloadLocalClasses = array(
'MediaWikiBagOStuff' => 'includes/BagOStuff.php',
'MediaWiki_I18N' => 'includes/SkinTemplate.php',
'MediaWiki' => 'includes/Wiki.php',
- 'memcached' => 'includes/memcached-client.php',
+ 'MemCachedClientforWiki' => 'includes/memcached-client.php',
'MessageCache' => 'includes/MessageCache.php',
'MimeMagic' => 'includes/MimeMagic.php',
'MWException' => 'includes/Exception.php',
+ 'MWMemcached' => 'includes/memcached-client.php',
'MWNamespace' => 'includes/Namespace.php',
- 'MySQLSearchResultSet' => 'includes/SearchMySQL.php',
'Namespace' => 'includes/NamespaceCompat.php', // Compat
'OldChangesList' => 'includes/ChangesList.php',
- 'OracleSearchResultSet' => 'includes/SearchOracle.php',
'OutputPage' => 'includes/OutputPage.php',
- 'PageHistory' => 'includes/PageHistory.php',
- 'PageHistoryPager' => 'includes/PageHistory.php',
'PageQueryPage' => 'includes/PageQueryPage.php',
+ 'PageHistory' => 'includes/HistoryPage.php',
+ 'PageHistoryPager' => 'includes/HistoryPage.php',
'Pager' => 'includes/Pager.php',
'PasswordError' => 'includes/User.php',
'PatrolLog' => 'includes/PatrolLog.php',
- 'PostgresSearchResult' => 'includes/SearchPostgres.php',
- 'PostgresSearchResultSet' => 'includes/SearchPostgres.php',
+ 'PoolCounter' => 'includes/PoolCounter.php',
+ 'PoolCounter_Stub' => 'includes/PoolCounter.php',
+ 'Preferences' => 'includes/Preferences.php',
'PrefixSearch' => 'includes/PrefixSearch.php',
'Profiler' => 'includes/Profiler.php',
'ProfilerSimple' => 'includes/ProfilerSimple.php',
@@ -161,20 +201,9 @@ $wgAutoloadLocalClasses = array(
'Revision' => 'includes/Revision.php',
'RSSFeed' => 'includes/Feed.php',
'Sanitizer' => 'includes/Sanitizer.php',
- 'SearchEngineDummy' => 'includes/SearchEngine.php',
- 'SearchEngine' => 'includes/SearchEngine.php',
- 'SearchHighlighter' => 'includes/SearchEngine.php',
- 'SearchMySQL4' => 'includes/SearchMySQL4.php',
- 'SearchMySQL' => 'includes/SearchMySQL.php',
- 'SearchOracle' => 'includes/SearchOracle.php',
- 'SearchPostgres' => 'includes/SearchPostgres.php',
- 'SearchResult' => 'includes/SearchEngine.php',
- 'SearchResultSet' => 'includes/SearchEngine.php',
- 'SearchResultTooMany' => 'includes/SearchEngine.php',
- 'SearchUpdate' => 'includes/SearchUpdate.php',
- 'SearchUpdateMyISAM' => 'includes/SearchUpdate.php',
'SiteConfiguration' => 'includes/SiteConfiguration.php',
'SiteStats' => 'includes/SiteStats.php',
+ 'SiteStatsInit' => 'includes/SiteStats.php',
'SiteStatsUpdate' => 'includes/SiteStats.php',
'Skin' => 'includes/Skin.php',
'SkinTemplate' => 'includes/SkinTemplate.php',
@@ -185,7 +214,13 @@ $wgAutoloadLocalClasses = array(
'SpecialRedirectToSpecial' => 'includes/SpecialPage.php',
'SqlBagOStuff' => 'includes/BagOStuff.php',
'SquidUpdate' => 'includes/SquidUpdate.php',
+ 'SquidPurgeClient' => 'includes/SquidPurgeClient.php',
+ 'SquidPurgeClientPool' => 'includes/SquidPurgeClient.php',
'Status' => 'includes/Status.php',
+ 'StubContLang' => 'includes/StubObject.php',
+ 'StubUser' => 'includes/StubObject.php',
+ 'StubUserLang' => 'includes/StubObject.php',
+ 'StubObject' => 'includes/StubObject.php',
'StringUtils' => 'includes/StringUtils.php',
'TablePager' => 'includes/Pager.php',
'ThumbnailImage' => 'includes/MediaTransformOutput.php',
@@ -193,15 +228,20 @@ $wgAutoloadLocalClasses = array(
'TitleDependency' => 'includes/CacheDependency.php',
'Title' => 'includes/Title.php',
'TitleArray' => 'includes/TitleArray.php',
+ 'TitleArrayFromResult' => 'includes/TitleArray.php',
'TitleListDependency' => 'includes/CacheDependency.php',
'TransformParameterError' => 'includes/MediaTransformOutput.php',
- 'TurckBagOStuff' => 'includes/BagOStuff.php',
'UnlistedSpecialPage' => 'includes/SpecialPage.php',
+ 'UploadBase' => 'includes/upload/UploadBase.php',
+ 'UploadFromStash' => 'includes/upload/UploadFromStash.php',
+ 'UploadFromFile' => 'includes/upload/UploadFromFile.php',
+ 'UploadFromUrl' => 'includes/upload/UploadFromUrl.php',
'User' => 'includes/User.php',
'UserArray' => 'includes/UserArray.php',
'UserArrayFromResult' => 'includes/UserArray.php',
'UserMailer' => 'includes/UserMailer.php',
'UserRightsProxy' => 'includes/UserRightsProxy.php',
+ 'WantedQueryPage' => 'includes/QueryPage.php',
'WatchedItem' => 'includes/WatchedItem.php',
'WatchlistEditor' => 'includes/WatchlistEditor.php',
'WebRequest' => 'includes/WebRequest.php',
@@ -209,6 +249,8 @@ $wgAutoloadLocalClasses = array(
'WikiError' => 'includes/WikiError.php',
'WikiErrorMsg' => 'includes/WikiError.php',
'WikiExporter' => 'includes/Export.php',
+ 'WikiMap' => 'includes/WikiMap.php',
+ 'WikiReference' => 'includes/WikiMap.php',
'WikiXmlError' => 'includes/WikiError.php',
'XCacheBagOStuff' => 'includes/BagOStuff.php',
'XmlDumpWriter' => 'includes/Export.php',
@@ -282,6 +324,7 @@ $wgAutoloadLocalClasses = array(
'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php',
'ApiQuerySearch' => 'includes/api/ApiQuerySearch.php',
'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php',
+ 'ApiQueryTags' => 'includes/api/ApiQueryTags.php',
'ApiQueryUserInfo' => 'includes/api/ApiQueryUserInfo.php',
'ApiQueryUsers' => 'includes/api/ApiQueryUsers.php',
'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php',
@@ -290,56 +333,58 @@ $wgAutoloadLocalClasses = array(
'ApiRollback' => 'includes/api/ApiRollback.php',
'ApiUnblock' => 'includes/api/ApiUnblock.php',
'ApiUndelete' => 'includes/api/ApiUndelete.php',
+ 'ApiUserrights' => 'includes/api/ApiUserrights.php',
+ 'ApiUpload' => 'includes/api/ApiUpload.php',
'ApiWatch' => 'includes/api/ApiWatch.php',
- 'Services_JSON' => 'includes/api/ApiFormatJson_json.php',
- 'Services_JSON_Error' => 'includes/api/ApiFormatJson_json.php',
+
'Spyc' => 'includes/api/ApiFormatYaml_spyc.php',
'UsageException' => 'includes/api/ApiMain.php',
+ # includes/json
+ 'Services_JSON' => 'includes/json/Services_JSON.php',
+ 'Services_JSON_Error' => 'includes/json/Services_JSON.php',
+ 'FormatJson' => 'includes/json/FormatJson.php',
+
# includes/db
'Blob' => 'includes/db/Database.php',
'ChronologyProtector' => 'includes/db/LBFactory.php',
- 'Database' => 'includes/db/Database.php',
+ 'Database' => 'includes/db/DatabaseMysql.php',
+ 'DatabaseBase' => 'includes/db/Database.php',
'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
- 'DatabaseMysql' => 'includes/db/Database.php',
+ 'DatabaseMysql' => 'includes/db/DatabaseMysql.php',
'DatabaseOracle' => 'includes/db/DatabaseOracle.php',
'DatabasePostgres' => 'includes/db/DatabasePostgres.php',
'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
+ 'DatabaseSqliteStandalone' => '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',
+ 'IBM_DB2Blob' => 'includes/db/DatabaseIbm_db2.php',
'LBFactory' => 'includes/db/LBFactory.php',
'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
'LBFactory_Simple' => 'includes/db/LBFactory.php',
+ 'LikeMatch' => 'includes/db/Database.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',
+ 'MySQLMasterPos' => 'includes/db/DatabaseMysql.php',
'ORABlob' => 'includes/db/DatabaseOracle.php',
+ 'ORAField' => 'includes/db/DatabaseOracle.php',
'ORAResult' => 'includes/db/DatabaseOracle.php',
'PostgresField' => 'includes/db/DatabasePostgres.php',
'ResultWrapper' => 'includes/db/Database.php',
'SQLiteField' => 'includes/db/DatabaseSqlite.php',
-
'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php',
'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php',
- 'IBM_DB2SearchResultSet' => 'includes/SearchIBM_DB2.php',
- 'SearchIBM_DB2' => 'includes/SearchIBM_DB2.php',
# includes/diff
- 'AncestorComparator' => 'includes/diff/HTMLDiff.php',
- 'AnchorToString' => 'includes/diff/HTMLDiff.php',
'ArrayDiffFormatter' => 'includes/diff/DifferenceEngine.php',
- 'BodyNode' => 'includes/diff/Nodes.php',
- 'ChangeText' => 'includes/diff/HTMLDiff.php',
- 'ChangeTextGenerator' => 'includes/diff/HTMLDiff.php',
- 'DelegatingContentHandler' => 'includes/diff/HTMLDiff.php',
'_DiffEngine' => 'includes/diff/DifferenceEngine.php',
- 'DifferenceEngine' => 'includes/diff/DifferenceEngine.php',
+ 'DifferenceEngine' => 'includes/diff/DifferenceInterface.php',
'DiffFormatter' => 'includes/diff/DifferenceEngine.php',
'Diff' => 'includes/diff/DifferenceEngine.php',
'_DiffOp_Add' => 'includes/diff/DifferenceEngine.php',
@@ -347,34 +392,17 @@ $wgAutoloadLocalClasses = array(
'_DiffOp_Copy' => 'includes/diff/DifferenceEngine.php',
'_DiffOp_Delete' => 'includes/diff/DifferenceEngine.php',
'_DiffOp' => 'includes/diff/DifferenceEngine.php',
- 'DomTreeBuilder' => 'includes/diff/HTMLDiff.php',
- 'DummyNode' => 'includes/diff/Nodes.php',
- 'HTMLDiffer' => 'includes/diff/HTMLDiff.php',
- 'HTMLOutput' => 'includes/diff/HTMLDiff.php',
'_HWLDF_WordAccumulator' => 'includes/diff/DifferenceEngine.php',
- 'ImageNode' => 'includes/diff/Nodes.php',
- 'LastCommonParentResult' => 'includes/diff/HTMLDiff.php',
'MappedDiff' => 'includes/diff/DifferenceEngine.php',
- 'Modification' => 'includes/diff/HTMLDiff.php',
- 'NoContentTagToString' => 'includes/diff/HTMLDiff.php',
- 'Node' => 'includes/diff/Nodes.php',
'RangeDifference' => 'includes/diff/Diff.php',
'TableDiffFormatter' => 'includes/diff/DifferenceEngine.php',
- 'TagNode' => 'includes/diff/Nodes.php',
- 'TagToString' => 'includes/diff/HTMLDiff.php',
- 'TagToStringFactory' => 'includes/diff/HTMLDiff.php',
- 'TextNode' => 'includes/diff/Nodes.php',
- 'TextNodeDiffer' => 'includes/diff/HTMLDiff.php',
- 'TextOnlyComparator' => 'includes/diff/HTMLDiff.php',
'UnifiedDiffFormatter' => 'includes/diff/DifferenceEngine.php',
- 'WhiteSpaceNode' => 'includes/diff/Nodes.php',
'WikiDiff3' => 'includes/diff/Diff.php',
'WordLevelDiff' => 'includes/diff/DifferenceEngine.php',
# includes/filerepo
'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
'File' => 'includes/filerepo/File.php',
- 'FileCache' => 'includes/filerepo/FileCache.php',
'FileRepo' => 'includes/filerepo/FileRepo.php',
'FileRepoStatus' => 'includes/filerepo/FileRepoStatus.php',
'ForeignAPIFile' => 'includes/filerepo/ForeignAPIFile.php',
@@ -408,10 +436,13 @@ $wgAutoloadLocalClasses = array(
# includes/parser
'CoreLinkFunctions' => 'includes/parser/CoreLinkFunctions.php',
'CoreParserFunctions' => 'includes/parser/CoreParserFunctions.php',
+ 'CoreTagHooks' => 'includes/parser/CoreTagHooks.php',
'DateFormatter' => 'includes/parser/DateFormatter.php',
'LinkHolderArray' => 'includes/parser/LinkHolderArray.php',
- 'LinkMarkerReplacer' => 'includes/parser/LinkMarkerReplacer.php',
+ 'LinkMarkerReplacer' => 'includes/parser/Parser_LinkHooks.php',
'OnlyIncludeReplacer' => 'includes/parser/Parser.php',
+ 'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
+ 'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
'PPDAccum_Hash' => 'includes/parser/Preprocessor_Hash.php',
'PPDPart' => 'includes/parser/Preprocessor_DOM.php',
'PPDPart_Hash' => 'includes/parser/Preprocessor_Hash.php',
@@ -442,7 +473,31 @@ $wgAutoloadLocalClasses = array(
'StripState' => 'includes/parser/Parser.php',
'MWTidy' => 'includes/parser/Tidy.php',
+ # includes/search
+ 'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php',
+ 'PostgresSearchResult' => 'includes/search/SearchPostgres.php',
+ 'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php',
+ 'SearchEngineDummy' => 'includes/search/SearchEngine.php',
+ 'SearchEngine' => 'includes/search/SearchEngine.php',
+ 'SearchHighlighter' => 'includes/search/SearchEngine.php',
+ 'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php',
+ 'SearchMySQL4' => 'includes/search/SearchMySQL4.php',
+ 'SearchMySQL' => 'includes/search/SearchMySQL.php',
+ 'SearchOracle' => 'includes/search/SearchOracle.php',
+ 'SearchPostgres' => 'includes/search/SearchPostgres.php',
+ 'SearchResult' => 'includes/search/SearchEngine.php',
+ 'SearchResultSet' => 'includes/search/SearchEngine.php',
+ 'SearchResultTooMany' => 'includes/search/SearchEngine.php',
+ 'SearchSqlite' => 'includes/search/SearchSqlite.php',
+ 'SearchUpdate' => 'includes/search/SearchUpdate.php',
+ 'SearchUpdateMyISAM' => 'includes/search/SearchUpdate.php',
+ 'SqliteSearchResultSet' => 'includes/search/SearchSqlite.php',
+ 'SqlSearchResultSet' => 'includes/search/SearchEngine.php',
+
# includes/specials
+ 'SpecialAllmessages' => 'includes/specials/SpecialAllmessages.php',
+ 'ActiveUsersPager' => 'includes/specials/SpecialActiveusers.php',
+ 'AllmessagesTablePager' => 'includes/specials/SpecialAllmessages.php',
'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php',
'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php',
'ContribsPager' => 'includes/specials/SpecialContributions.php',
@@ -456,6 +511,7 @@ $wgAutoloadLocalClasses = array(
'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php',
'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php',
'EmailUserForm' => 'includes/specials/SpecialEmailuser.php',
+ 'FakeResultWrapper' => 'includes/specials/SpecialAllmessages.php',
'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php',
'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php',
'IPBlockForm' => 'includes/specials/SpecialBlockip.php',
@@ -482,26 +538,41 @@ $wgAutoloadLocalClasses = array(
'PageArchive' => 'includes/specials/SpecialUndelete.php',
'SpecialResetpass' => 'includes/specials/SpecialResetpass.php',
'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php',
- 'PreferencesForm' => 'includes/specials/SpecialPreferences.php',
+ 'PreferencesForm' => 'includes/Preferences.php',
'RandomPage' => 'includes/specials/SpecialRandompage.php',
'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php',
'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_RevisionList' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_RevisionItem' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_ArchiveList' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_ArchiveItem' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_FileList' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_FileItem' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_ArchivedFileList' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_ArchivedFileItem' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_LogList' => 'includes/specials/SpecialRevisiondelete.php',
+ 'RevDel_LogItem' => 'includes/specials/SpecialRevisiondelete.php',
'ShortPagesPage' => 'includes/specials/SpecialShortpages.php',
+ 'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php',
'SpecialAllpages' => 'includes/specials/SpecialAllpages.php',
+ 'SpecialBlankpage' => 'includes/specials/SpecialBlankpage.php',
'SpecialBookSources' => 'includes/specials/SpecialBooksources.php',
'SpecialExport' => 'includes/specials/SpecialExport.php',
'SpecialImport' => 'includes/specials/SpecialImport.php',
'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php',
'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php',
+ 'SpecialPreferences' => 'includes/specials/SpecialPreferences.php',
'SpecialPrefixindex' => 'includes/specials/SpecialPrefixindex.php',
'SpecialRandomredirect' => 'includes/specials/SpecialRandomredirect.php',
- 'SpecialRecentchanges' => 'includes/specials/SpecialRecentchanges.php',
+ 'SpecialRecentChanges' => 'includes/specials/SpecialRecentchanges.php',
'SpecialRecentchangeslinked' => 'includes/specials/SpecialRecentchangeslinked.php',
'SpecialSearch' => 'includes/specials/SpecialSearch.php',
- 'SpecialSearchOld' => 'includes/specials/SpecialSearch.php',
'SpecialStatistics' => 'includes/specials/SpecialStatistics.php',
'SpecialTags' => 'includes/specials/SpecialTags.php',
+ 'SpecialUpload' => 'includes/specials/SpecialUpload.php',
'SpecialVersion' => 'includes/specials/SpecialVersion.php',
+ 'SpecialWhatlinkshere' => 'includes/specials/SpecialWhatlinkshere.php',
+ 'SpecialWhatLinksHere' => 'includes/specials/SpecialWhatlinkshere.php',
'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php',
'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php',
'UncategorizedTemplatesPage' => 'includes/specials/SpecialUncategorizedtemplates.php',
@@ -511,7 +582,7 @@ $wgAutoloadLocalClasses = array(
'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php',
'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php',
'UploadForm' => 'includes/specials/SpecialUpload.php',
- 'UploadFormMogile' => 'includes/specials/SpecialUploadMogile.php',
+ 'UploadSourceField' => 'includes/specials/SpecialUpload.php',
'UserrightsPage' => 'includes/specials/SpecialUserrights.php',
'UsersPager' => 'includes/specials/SpecialListusers.php',
'WantedCategoriesPage' => 'includes/specials/SpecialWantedcategories.php',
@@ -530,13 +601,14 @@ $wgAutoloadLocalClasses = array(
# languages
'Language' => 'languages/Language.php',
'FakeConverter' => 'languages/Language.php',
+ 'LanguageConverter' => 'languages/LanguageConverter.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',
+ 'SevenZipStream' => 'maintenance/7zip.inc',
);
@@ -544,7 +616,7 @@ class AutoLoader {
/**
* autoload - take a class name and attempt to load it
*
- * @param string $className Name of class we're looking for.
+ * @param $className String: 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.
@@ -567,7 +639,7 @@ class AutoLoader {
}
}
if ( !$filename ) {
- if( function_exists( 'wfDebug' ) )
+ if( function_exists( 'wfDebug' ) )
wfDebug( "Class {$className} not found; skipped loading\n" );
# Give up
return false;
@@ -592,6 +664,17 @@ class AutoLoader {
}
}
}
+
+ /**
+ * Force a class to be run through the autoloader, helpful for things like
+ * Sanitizer that have define()s outside of their class definition. Of course
+ * this wouldn't be necessary if everything in MediaWiki was class-based. Sigh.
+ *
+ * @return Boolean Return the results of class_exists() so we know if we were successful
+ */
+ static function loadClass( $class ) {
+ return class_exists( $class );
+ }
}
function wfLoadAllExtensions() {
@@ -604,4 +687,6 @@ if ( function_exists( 'spl_autoload_register' ) ) {
function __autoload( $class ) {
AutoLoader::autoload( $class );
}
+
+ ini_set( 'unserialize_callback_func', '__autoload' );
}
diff --git a/includes/Autopromote.php b/includes/Autopromote.php
index c8a4c03b..c0adff43 100644
--- a/includes/Autopromote.php
+++ b/includes/Autopromote.php
@@ -1,5 +1,4 @@
<?php
-
/**
* This class checks if user can get extra rights
* because of conditions specified in $wgAutopromote
@@ -18,9 +17,9 @@ class Autopromote {
if( self::recCheckCondition( $cond, $user ) )
$promote[] = $group;
}
-
+
wfRunHooks( 'GetAutoPromoteGroups', array( $user, &$promote ) );
-
+
return $promote;
}
@@ -116,6 +115,8 @@ class Autopromote {
return $cond[1] == wfGetIP();
case APCOND_IPINRANGE:
return IP::isInRange( wfGetIP(), $cond[1] );
+ case APCOND_BLOCKED:
+ return $user->isBlocked();
default:
$result = null;
wfRunHooks( 'AutopromoteCondition', array( $cond[0], array_slice( $cond, 1 ), $user, &$result ) );
diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php
index a7bcd858..53f92dd9 100644
--- a/includes/BacklinkCache.php
+++ b/includes/BacklinkCache.php
@@ -1,10 +1,9 @@
<?php
-
/**
* Class for fetching backlink lists, approximate backlink counts and partitions.
* Instances of this class should typically be fetched with $title->getBacklinkCache().
*
- * Ideally you should only get your backlinks from here when you think there is some
+ * Ideally you should only get your backlinks from here when you think there is some
* advantage in caching them. Otherwise it's just a waste of memory.
*/
class BacklinkCache {
@@ -47,44 +46,53 @@ class BacklinkCache {
/**
* Get the backlinks for a given table. Cached in process memory only.
- * @param string $table
+ * @param $table String
+ * @param $startId Integer or false
+ * @param $endId Integer or false
* @return TitleArray
*/
public function getLinks( $table, $startId = false, $endId = false ) {
wfProfileIn( __METHOD__ );
+ $fromField = $this->getPrefix( $table ) . '_from';
+
if ( $startId || $endId ) {
// Partial range, not cached
- wfDebug( __METHOD__.": from DB (uncacheable range)\n" );
+ wfDebug( __METHOD__ . ": from DB (uncacheable range)\n" );
$conds = $this->getConditions( $table );
// Use the from field in the condition rather than the joined page_id,
// because databases are stupid and don't necessarily propagate indexes.
- $fromField = $this->getPrefix( $table ) . '_from';
if ( $startId ) {
$conds[] = "$fromField >= " . intval( $startId );
}
if ( $endId ) {
$conds[] = "$fromField <= " . intval( $endId );
}
- $res = $this->getDB()->select(
+ $res = $this->getDB()->select(
array( $table, 'page' ),
- array( 'page_namespace', 'page_title', 'page_id'),
+ array( 'page_namespace', 'page_title', 'page_id' ),
$conds,
__METHOD__,
- array('STRAIGHT_JOIN') );
+ array(
+ 'STRAIGHT_JOIN',
+ 'ORDER BY' => $fromField
+ ) );
$ta = TitleArray::newFromResult( $res );
wfProfileOut( __METHOD__ );
return $ta;
}
if ( !isset( $this->fullResultCache[$table] ) ) {
- wfDebug( __METHOD__.": from DB\n" );
- $res = $this->getDB()->select(
+ wfDebug( __METHOD__ . ": from DB\n" );
+ $res = $this->getDB()->select(
array( $table, 'page' ),
array( 'page_namespace', 'page_title', 'page_id' ),
$this->getConditions( $table ),
__METHOD__,
- array('STRAIGHT_JOIN') );
+ array(
+ 'STRAIGHT_JOIN',
+ 'ORDER BY' => $fromField,
+ ) );
$this->fullResultCache[$table] = $res;
}
$ta = TitleArray::newFromResult( $this->fullResultCache[$table] );
@@ -103,6 +111,7 @@ class BacklinkCache {
'templatelinks' => 'tl',
'redirect' => 'rd',
);
+
if ( isset( $prefixes[$table] ) ) {
return $prefixes[$table];
} else {
@@ -115,6 +124,7 @@ class BacklinkCache {
*/
protected function getConditions( $table ) {
$prefix = $this->getPrefix( $table );
+
switch ( $table ) {
case 'pagelinks':
case 'templatelinks':
@@ -126,13 +136,13 @@ class BacklinkCache {
);
break;
case 'imagelinks':
- $conds = array(
+ $conds = array(
'il_to' => $this->title->getDBkey(),
'page_id=il_from'
);
break;
case 'categorylinks':
- $conds = array(
+ $conds = array(
'cl_to' => $this->title->getDBkey(),
'page_id=cl_from',
);
@@ -150,10 +160,12 @@ class BacklinkCache {
if ( isset( $this->fullResultCache[$table] ) ) {
return $this->fullResultCache[$table]->numRows();
}
+
if ( isset( $this->partitionCache[$table] ) ) {
$entry = reset( $this->partitionCache[$table] );
return $entry['numRows'];
}
+
$titleArray = $this->getLinks( $table );
return $titleArray->count();
}
@@ -163,33 +175,40 @@ class BacklinkCache {
* Returns an array giving the start and end of each range. The first batch has
* a start of false, and the last batch has an end of false.
*
- * @param string $table The links table name
- * @param integer $batchSize
- * @return array
+ * @param $table String: the links table name
+ * @param $batchSize Integer
+ * @return Array
*/
public function partition( $table, $batchSize ) {
// Try cache
if ( isset( $this->partitionCache[$table][$batchSize] ) ) {
- wfDebug( __METHOD__.": got from partition cache\n" );
+ wfDebug( __METHOD__ . ": got from partition cache\n" );
return $this->partitionCache[$table][$batchSize]['batches'];
}
+
$this->partitionCache[$table][$batchSize] = false;
$cacheEntry =& $this->partitionCache[$table][$batchSize];
// Try full result cache
if ( isset( $this->fullResultCache[$table] ) ) {
$cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
- wfDebug( __METHOD__.": got from full result cache\n" );
+ wfDebug( __METHOD__ . ": got from full result cache\n" );
return $cacheEntry['batches'];
}
+
// Try memcached
global $wgMemc;
- $memcKey = wfMemcKey( 'backlinks', md5( $this->title->getPrefixedDBkey() ),
- $table, $batchSize );
+ $memcKey = wfMemcKey(
+ 'backlinks',
+ md5( $this->title->getPrefixedDBkey() ),
+ $table,
+ $batchSize
+ );
$memcValue = $wgMemc->get( $memcKey );
+
if ( is_array( $memcValue ) ) {
$cacheEntry = $memcValue;
- wfDebug( __METHOD__.": got from memcached $memcKey\n" );
+ wfDebug( __METHOD__ . ": got from memcached $memcKey\n" );
return $cacheEntry['batches'];
}
// Fetch from database
@@ -197,17 +216,18 @@ class BacklinkCache {
$cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize );
// Save to memcached
$wgMemc->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY );
- wfDebug( __METHOD__.": got from database\n" );
+ wfDebug( __METHOD__ . ": got from database\n" );
return $cacheEntry['batches'];
}
- /**
+ /**
* Partition a DB result with backlinks in it into batches
*/
protected function partitionResult( $res, $batchSize ) {
$batches = array();
$numRows = $res->numRows();
$numBatches = ceil( $numRows / $batchSize );
+
for ( $i = 0; $i < $numBatches; $i++ ) {
if ( $i == 0 ) {
$start = false;
@@ -217,6 +237,7 @@ class BacklinkCache {
$row = $res->fetchObject();
$start = $row->page_id;
}
+
if ( $i == $numBatches - 1 ) {
$end = false;
} else {
@@ -225,6 +246,12 @@ class BacklinkCache {
$row = $res->fetchObject();
$end = $row->page_id - 1;
}
+
+ # Sanity check order
+ if ( $start && $end && $start > $end ) {
+ throw new MWException( __METHOD__ . ': Internal error: query result out of order' );
+ }
+
$batches[] = array( $start, $end );
}
return array( 'numRows' => $numRows, 'batches' => $batches );
diff --git a/includes/BagOStuff.php b/includes/BagOStuff.php
index ffa8a0bb..ac0263d8 100644
--- a/includes/BagOStuff.php
+++ b/includes/BagOStuff.php
@@ -32,134 +32,129 @@
* backends for local hash array and SQL table included:
* <code>
* $bag = new HashBagOStuff();
- * $bag = new MediaWikiBagOStuff($tablename); # connect to db first
+ * $bag = new SqlBagOStuff(); # connect to db first
* </code>
*
* @ingroup Cache
*/
-class BagOStuff {
- var $debugmode;
+abstract class BagOStuff {
+ var $debugMode = false;
- function __construct() {
- $this->set_debug( false );
- }
-
- function set_debug($bool) {
- $this->debugmode = $bool;
+ public function set_debug( $bool ) {
+ $this->debugMode = $bool;
}
/* *** THE GUTS OF THE OPERATION *** */
/* Override these with functional things in subclasses */
- function get($key) {
- /* stub */
- return false;
- }
+ /**
+ * Get an item with the given key. Returns false if it does not exist.
+ * @param $key string
+ */
+ abstract public function get( $key );
- function set($key, $value, $exptime=0) {
- /* stub */
- return false;
- }
+ /**
+ * Set an item.
+ * @param $key string
+ * @param $value mixed
+ * @param $exptime int Either an interval in seconds or a unix timestamp for expiry
+ */
+ abstract public function set( $key, $value, $exptime = 0 );
- function delete($key, $time=0) {
- /* stub */
- return false;
- }
+ /*
+ * Delete an item.
+ * @param $key string
+ * @param $time int Amount of time to delay the operation (mostly memcached-specific)
+ */
+ abstract public function delete( $key, $time = 0 );
- function lock($key, $timeout = 0) {
+ public function lock( $key, $timeout = 0 ) {
/* stub */
return true;
}
- function unlock($key) {
+ public function unlock( $key ) {
/* stub */
return true;
}
- function keys() {
+ public function keys() {
/* stub */
return array();
}
/* *** Emulated functions *** */
/* Better performance can likely be got with custom written versions */
- function get_multi($keys) {
+ public function get_multi( $keys ) {
$out = array();
- foreach($keys as $key)
- $out[$key] = $this->get($key);
+
+ foreach ( $keys as $key ) {
+ $out[$key] = $this->get( $key );
+ }
+
return $out;
}
- function set_multi($hash, $exptime=0) {
- foreach($hash as $key => $value)
- $this->set($key, $value, $exptime);
+ public function set_multi( $hash, $exptime = 0 ) {
+ foreach ( $hash as $key => $value ) {
+ $this->set( $key, $value, $exptime );
+ }
}
- function add($key, $value, $exptime=0) {
- if( $this->get($key) == false ) {
- $this->set($key, $value, $exptime);
+ public function add( $key, $value, $exptime = 0 ) {
+ if ( $this->get( $key ) == false ) {
+ $this->set( $key, $value, $exptime );
return true;
}
}
- function add_multi($hash, $exptime=0) {
- foreach($hash as $key => $value)
- $this->add($key, $value, $exptime);
+ public function add_multi( $hash, $exptime = 0 ) {
+ foreach ( $hash as $key => $value ) {
+ $this->add( $key, $value, $exptime );
+ }
}
- function delete_multi($keys, $time=0) {
- foreach($keys as $key)
- $this->delete($key, $time);
+ public function delete_multi( $keys, $time = 0 ) {
+ foreach ( $keys as $key ) {
+ $this->delete( $key, $time );
+ }
}
- function replace($key, $value, $exptime=0) {
- if( $this->get($key) !== false )
- $this->set($key, $value, $exptime);
+ public function replace( $key, $value, $exptime = 0 ) {
+ if ( $this->get( $key ) !== false ) {
+ $this->set( $key, $value, $exptime );
+ }
}
- function incr($key, $value=1) {
- if ( !$this->lock($key) ) {
+ public function incr( $key, $value = 1 ) {
+ if ( !$this->lock( $key ) ) {
return false;
}
- $value = intval($value);
- if($value < 0) $value = 0;
+ $value = intval( $value );
$n = false;
- if( ($n = $this->get($key)) !== false ) {
+ if ( ( $n = $this->get( $key ) ) !== false ) {
$n += $value;
- $this->set($key, $n); // exptime?
+ $this->set( $key, $n ); // exptime?
}
- $this->unlock($key);
+ $this->unlock( $key );
return $n;
}
- function decr($key, $value=1) {
- if ( !$this->lock($key) ) {
- return false;
- }
- $value = intval($value);
- if($value < 0) $value = 0;
-
- $m = false;
- if( ($n = $this->get($key)) !== false ) {
- $m = $n - $value;
- if($m < 0) $m = 0;
- $this->set($key, $m); // exptime?
- }
- $this->unlock($key);
- return $m;
+ public function decr( $key, $value = 1 ) {
+ return $this->incr( $key, - $value );
}
- function _debug($text) {
- if($this->debugmode)
- wfDebug("BagOStuff debug: $text\n");
+ public function debug( $text ) {
+ if ( $this->debugMode )
+ wfDebug( "BagOStuff debug: $text\n" );
}
/**
* Convert an optionally relative time to an absolute time
*/
- static function convertExpiry( $exptime ) {
- if(($exptime != 0) && ($exptime < 3600*24*30)) {
+ protected function convertExpiry( $exptime ) {
+ if ( ( $exptime != 0 ) && ( $exptime < 3600 * 24 * 30 ) ) {
return time() + $exptime;
} else {
return $exptime;
@@ -167,7 +162,6 @@ class BagOStuff {
}
}
-
/**
* Functional versions!
* This is a test of the interface, mainly. It stores things in an associative
@@ -182,30 +176,34 @@ class HashBagOStuff extends BagOStuff {
$this->bag = array();
}
- function _expire($key) {
+ protected function expire( $key ) {
$et = $this->bag[$key][1];
- if(($et == 0) || ($et > time()))
+ if ( ( $et == 0 ) || ( $et > time() ) ) {
return false;
- $this->delete($key);
+ }
+ $this->delete( $key );
return true;
}
- function get($key) {
- if(!$this->bag[$key])
+ function get( $key ) {
+ if ( !isset( $this->bag[$key] ) ) {
return false;
- if($this->_expire($key))
+ }
+ if ( $this->expire( $key ) ) {
return false;
+ }
return $this->bag[$key][0];
}
- function set($key,$value,$exptime=0) {
- $this->bag[$key] = array( $value, BagOStuff::convertExpiry( $exptime ) );
+ function set( $key, $value, $exptime = 0 ) {
+ $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) );
}
- function delete($key,$time=0) {
- if(!$this->bag[$key])
+ function delete( $key, $time = 0 ) {
+ if ( !isset( $this->bag[$key] ) ) {
return false;
- unset($this->bag[$key]);
+ }
+ unset( $this->bag[$key] );
return true;
}
@@ -215,182 +213,196 @@ class HashBagOStuff extends BagOStuff {
}
/**
- * Generic class to store objects in a database
+ * Class to store objects in the database
*
* @ingroup Cache
*/
-abstract class SqlBagOStuff extends BagOStuff {
- var $table;
- var $lastexpireall = 0;
+class SqlBagOStuff extends BagOStuff {
+ var $lb, $db;
+ var $lastExpireAll = 0;
- /**
- * Constructor
- *
- * @param $tablename String: name of the table to use
- */
- function __construct($tablename = 'objectcache') {
- $this->table = $tablename;
+ protected function getDB() {
+ global $wgDBtype;
+ if ( !isset( $this->db ) ) {
+ /* We must keep a separate connection to MySQL in order to avoid deadlocks
+ * However, SQLite has an opposite behaviour.
+ * @todo Investigate behaviour for other databases
+ */
+ if ( $wgDBtype == 'sqlite' ) {
+ $this->db = wfGetDB( DB_MASTER );
+ } else {
+ $this->lb = wfGetLBFactory()->newMainLB();
+ $this->db = $this->lb->getConnection( DB_MASTER );
+ $this->db->clearFlag( DBO_TRX );
+ }
+ }
+ return $this->db;
}
- function get($key) {
- /* expire old entries if any */
+ public function get( $key ) {
+ # expire old entries if any
$this->garbageCollect();
-
- $res = $this->_query(
- "SELECT value,exptime FROM $0 WHERE keyname='$1'", $key);
- if(!$res) {
- $this->_debug("get: ** error: " . $this->_dberror($res) . " **");
+ $db = $this->getDB();
+ $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ),
+ array( 'keyname' => $key ), __METHOD__ );
+ if ( !$row ) {
+ $this->debug( 'get: no matching rows' );
return false;
}
- 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() )
- {
- $this->_debug("get: key has expired, deleting");
- $this->delete($key);
- return false;
+
+ $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
+ if ( $this->isExpired( $row->exptime ) ) {
+ $this->debug( "get: key has expired, deleting" );
+ try {
+ $db->begin();
+ # Put the expiry time in the WHERE condition to avoid deleting a
+ # newly-inserted value
+ $db->delete( 'objectcache',
+ array(
+ 'keyname' => $key,
+ 'exptime' => $row->exptime
+ ), __METHOD__ );
+ $db->commit();
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
}
- return $this->_unserialize($this->_blobdecode($row->value));
- } else {
- $this->_debug('get: no matching rows');
+ return false;
}
- return false;
+ return $this->unserialize( $db->decodeBlob( $row->value ) );
}
- function set($key,$value,$exptime=0) {
- if ( $this->_readonly() ) {
- return false;
- }
- $exptime = intval($exptime);
- if($exptime < 0) $exptime = 0;
- if($exptime == 0) {
- $exp = $this->_maxdatetime();
+ public function set( $key, $value, $exptime = 0 ) {
+ $db = $this->getDB();
+ $exptime = intval( $exptime );
+ if ( $exptime < 0 ) $exptime = 0;
+ if ( $exptime == 0 ) {
+ $encExpiry = $this->getMaxDateTime();
} else {
- if($exptime < 3.16e8) # ~10 years
+ if ( $exptime < 3.16e8 ) # ~10 years
$exptime += time();
- $exp = $this->_fromunixtime($exptime);
+ $encExpiry = $db->timestamp( $exptime );
}
- $this->_begin();
- $this->_query(
- "DELETE FROM $0 WHERE keyname='$1'", $key );
- $this->_doinsert($this->getTableName(), array(
+ try {
+ $db->begin();
+ $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
+ $db->insert( 'objectcache',
+ array(
'keyname' => $key,
- 'value' => $this->_blobencode($this->_serialize($value)),
- 'exptime' => $exp
- ));
- $this->_commit();
- return true; /* ? */
+ 'value' => $db->encodeBlob( $this->serialize( $value ) ),
+ 'exptime' => $encExpiry
+ ), __METHOD__ );
+ $db->commit();
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
+ return false;
+ }
+ return true;
}
- function delete($key,$time=0) {
- if ( $this->_readonly() ) {
+ public function delete( $key, $time = 0 ) {
+ $db = $this->getDB();
+ try {
+ $db->begin();
+ $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
+ $db->commit();
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
return false;
}
- $this->_begin();
- $this->_query(
- "DELETE FROM $0 WHERE keyname='$1'", $key );
- $this->_commit();
- return true; /* ? */
+ return true;
}
- function keys() {
- $res = $this->_query( "SELECT keyname FROM $0" );
- if(!$res) {
- $this->_debug("keys: ** error: " . $this->_dberror($res) . " **");
- return array();
+ public function incr( $key, $step = 1 ) {
+ $db = $this->getDB();
+ $step = intval( $step );
+
+ try {
+ $db->begin();
+ $row = $db->selectRow( 'objectcache', array( 'value', 'exptime' ),
+ array( 'keyname' => $key ), __METHOD__, array( 'FOR UPDATE' ) );
+ if ( $row === false ) {
+ // Missing
+ $db->commit();
+ return false;
+ }
+ $db->delete( 'objectcache', array( 'keyname' => $key ), __METHOD__ );
+ if ( $this->isExpired( $row->exptime ) ) {
+ // Expired, do not reinsert
+ $db->commit();
+ return false;
+ }
+
+ $oldValue = intval( $this->unserialize( $db->decodeBlob( $row->value ) ) );
+ $newValue = $oldValue + $step;
+ $db->insert( 'objectcache',
+ array(
+ 'keyname' => $key,
+ 'value' => $db->encodeBlob( $this->serialize( $newValue ) ),
+ 'exptime' => $row->exptime
+ ), __METHOD__ );
+ $db->commit();
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
+ return false;
}
+ return $newValue;
+ }
+
+ public function keys() {
+ $db = $this->getDB();
+ $res = $db->select( 'objectcache', array( 'keyname' ), false, __METHOD__ );
$result = array();
- while( $row = $this->_fetchobject($res) ) {
+ foreach ( $res as $row ) {
$result[] = $row->keyname;
}
return $result;
}
- function getTableName() {
- return $this->table;
+ protected function isExpired( $exptime ) {
+ return $exptime != $this->getMaxDateTime() && wfTimestamp( TS_UNIX, $exptime ) < time();
}
- function _query($sql) {
- $reps = func_get_args();
- $reps[0] = $this->getTableName();
- // ewwww
- for($i=0;$i<count($reps);$i++) {
- $sql = str_replace(
- '$' . $i,
- $i > 0 ? $this->_strencode($reps[$i]) : $reps[$i],
- $sql);
- }
- $res = $this->_doquery($sql);
- if($res == false) {
- $this->_debug('query failed: ' . $this->_dberror($res));
+ protected function getMaxDateTime() {
+ if ( time() > 0x7fffffff ) {
+ return $this->getDB()->timestamp( 1 << 62 );
+ } else {
+ return $this->getDB()->timestamp( 0x7fffffff );
}
- return $res;
- }
-
- function _strencode($str) {
- /* Protect strings in SQL */
- return str_replace( "'", "''", $str );
- }
- function _blobencode($str) {
- return $str;
- }
- function _blobdecode($str) {
- return $str;
- }
-
- abstract function _doinsert($table, $vals);
- abstract function _doquery($sql);
-
- abstract function _readonly();
-
- function _begin() {}
- function _commit() {}
-
- function _freeresult($result) {
- /* stub */
- return false;
}
- function _dberror($result) {
- /* stub */
- return 'unknown error';
- }
-
- abstract function _maxdatetime();
- abstract function _fromunixtime($ts);
-
- function garbageCollect() {
+ protected function garbageCollect() {
/* Ignore 99% of requests */
if ( !mt_rand( 0, 100 ) ) {
- $nowtime = time();
+ $now = time();
/* Avoid repeating the delete within a few seconds */
- if ( $nowtime > ($this->lastexpireall + 1) ) {
- $this->lastexpireall = $nowtime;
- $this->expireall();
+ if ( $now > ( $this->lastExpireAll + 1 ) ) {
+ $this->lastExpireAll = $now;
+ $this->expireAll();
}
}
}
- function expireall() {
- /* Remove any items that have expired */
- if ( $this->_readonly() ) {
- return false;
+ public function expireAll() {
+ $db = $this->getDB();
+ $now = $db->timestamp();
+ try {
+ $db->begin();
+ $db->delete( 'objectcache', array( 'exptime < ' . $db->addQuotes( $now ) ), __METHOD__ );
+ $db->commit();
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
}
- $now = $this->_fromunixtime( time() );
- $this->_begin();
- $this->_query( "DELETE FROM $0 WHERE exptime < '$now'" );
- $this->_commit();
}
- function deleteall() {
- /* Clear *all* items from cache table */
- if ( $this->_readonly() ) {
- return false;
+ public function deleteAll() {
+ $db = $this->getDB();
+ try {
+ $db->begin();
+ $db->delete( 'objectcache', '*', __METHOD__ );
+ $db->commit();
+ } catch ( DBQueryError $e ) {
+ $this->handleWriteError( $e );
}
- $this->_begin();
- $this->_query( "DELETE FROM $0" );
- $this->_commit();
}
/**
@@ -401,9 +413,9 @@ abstract class SqlBagOStuff extends BagOStuff {
* @param $data mixed
* @return string
*/
- function _serialize( &$data ) {
+ protected function serialize( &$data ) {
$serial = serialize( $data );
- if( function_exists( 'gzdeflate' ) ) {
+ if ( function_exists( 'gzdeflate' ) ) {
return gzdeflate( $serial );
} else {
return $serial;
@@ -415,156 +427,39 @@ abstract class SqlBagOStuff extends BagOStuff {
* @param $serial string
* @return mixed
*/
- function _unserialize( $serial ) {
- if( function_exists( 'gzinflate' ) ) {
+ protected function unserialize( $serial ) {
+ if ( function_exists( 'gzinflate' ) ) {
$decomp = @gzinflate( $serial );
- if( false !== $decomp ) {
+ if ( false !== $decomp ) {
$serial = $decomp;
}
}
$ret = unserialize( $serial );
return $ret;
}
-}
-/**
- * Stores objects in the main database of the wiki
- *
- * @ingroup Cache
- */
-class MediaWikiBagOStuff extends SqlBagOStuff {
- var $tableInitialised = false;
- var $lb, $db;
-
- function _getDB(){
- global $wgDBtype;
- if ( !isset( $this->db ) ) {
- /* We must keep a separate connection to MySQL in order to avoid deadlocks
- * However, SQLite has an opposite behaviour.
- * @todo Investigate behaviour for other databases
- */
- if ( $wgDBtype == 'sqlite' ) {
- $this->db = wfGetDB( DB_MASTER );
- } else {
- $this->lb = wfGetLBFactory()->newMainLB();
- $this->db = $this->lb->getConnection( DB_MASTER );
- $this->db->clearFlag( DBO_TRX );
- }
- }
- return $this->db;
- }
- function _begin() {
- $this->_getDB()->begin();
- }
- function _commit() {
- $this->_getDB()->commit();
- }
- function _doquery($sql) {
- return $this->_getDB()->query( $sql, __METHOD__ );
- }
- function _doinsert($t, $v) {
- return $this->_getDB()->insert($t, $v, __METHOD__, array( 'IGNORE' ) );
- }
- function _fetchobject($result) {
- return $this->_getDB()->fetchObject($result);
- }
- function _freeresult($result) {
- return $this->_getDB()->freeResult($result);
- }
- function _dberror($result) {
- return $this->_getDB()->lastError();
- }
- function _maxdatetime() {
- if ( time() > 0x7fffffff ) {
- return $this->_fromunixtime( 1<<62 );
- } else {
- return $this->_fromunixtime( 0x7fffffff );
- }
- }
- function _fromunixtime($ts) {
- return $this->_getDB()->timestamp($ts);
- }
- /***
- * Note -- this should *not* check wfReadOnly().
- * Read-only mode has been repurposed from the original
- * "nothing must write to the database" to "users should not
- * be able to edit or alter anything user-visible".
- *
- * Backend bits like the object cache should continue
- * to work in this mode, otherwise things will blow up
- * like the message cache failing to save its state,
- * causing long delays (bug 11533).
+ /**
+ * Handle a DBQueryError which occurred during a write operation.
+ * Ignore errors which are due to a read-only database, rethrow others.
*/
- function _readonly(){
- return false;
- }
- function _strencode($s) {
- return $this->_getDB()->strencode($s);
- }
- function _blobencode($s) {
- return $this->_getDB()->encodeBlob($s);
- }
- function _blobdecode($s) {
- return $this->_getDB()->decodeBlob($s);
- }
- function getTableName() {
- if ( !$this->tableInitialised ) {
- $dbw = $this->_getDB();
- /* This is actually a hack, we should be able
- to use Language classes here... or not */
- if (!$dbw)
- throw new MWException("Could not connect to database");
- $this->table = $dbw->tableName( $this->table );
- $this->tableInitialised = true;
+ protected function handleWriteError( $exception ) {
+ $db = $this->getDB();
+ if ( !$db->wasReadOnlyError() ) {
+ throw $exception;
}
- return $this->table;
+ try {
+ $db->rollback();
+ } catch ( DBQueryError $e ) {
+ }
+ wfDebug( __METHOD__ . ": ignoring query error\n" );
+ $db->ignoreErrors( false );
}
}
/**
- * This is a wrapper for Turck MMCache's shared memory functions.
- *
- * You can store objects with mmcache_put() and mmcache_get(), but Turck seems
- * to use a weird custom serializer that randomly segfaults. So we wrap calls
- * with serialize()/unserialize().
- *
- * The thing I noticed about the Turck serialized data was that unlike ordinary
- * serialize(), it contained the names of methods, and judging by the amount of
- * binary data, perhaps even the bytecode of the methods themselves. It may be
- * that Turck's serializer is faster, so a possible future extension would be
- * to use it for arrays but not for objects.
- *
- * @ingroup Cache
+ * Backwards compatibility alias
*/
-class TurckBagOStuff extends BagOStuff {
- function get($key) {
- $val = mmcache_get( $key );
- if ( is_string( $val ) ) {
- $val = unserialize( $val );
- }
- return $val;
- }
-
- function set($key, $value, $exptime=0) {
- mmcache_put( $key, serialize( $value ), $exptime );
- return true;
- }
-
- function delete($key, $time=0) {
- mmcache_rm( $key );
- return true;
- }
-
- function lock($key, $waitTimeout = 0 ) {
- mmcache_lock( $key );
- return true;
- }
-
- function unlock($key) {
- mmcache_unlock( $key );
- return true;
- }
-}
+class MediaWikiBagOStuff extends SqlBagOStuff { }
/**
* This is a wrapper for APC's shared memory functions
@@ -572,36 +467,45 @@ class TurckBagOStuff extends BagOStuff {
* @ingroup Cache
*/
class APCBagOStuff extends BagOStuff {
- function get($key) {
- $val = apc_fetch($key);
+ public function get( $key ) {
+ $val = apc_fetch( $key );
if ( is_string( $val ) ) {
$val = unserialize( $val );
}
return $val;
}
- function set($key, $value, $exptime=0) {
- apc_store($key, serialize($value), $exptime);
+ public function set( $key, $value, $exptime = 0 ) {
+ apc_store( $key, serialize( $value ), $exptime );
return true;
}
- function delete($key, $time=0) {
- apc_delete($key);
+ public function delete( $key, $time = 0 ) {
+ apc_delete( $key );
return true;
}
-}
+ public function keys() {
+ $info = apc_cache_info( 'user' );
+ $list = $info['cache_list'];
+ $keys = array();
+ foreach ( $list as $entry ) {
+ $keys[] = $entry['info'];
+ }
+ return $keys;
+ }
+}
/**
* This is a wrapper for eAccelerator's shared memory functions.
*
- * This is basically identical to the Turck MMCache version,
+ * This is basically identical to the deceased Turck MMCache version,
* mostly because eAccelerator is based on Turck MMCache.
*
* @ingroup Cache
*/
class eAccelBagOStuff extends BagOStuff {
- function get($key) {
+ public function get( $key ) {
$val = eaccelerator_get( $key );
if ( is_string( $val ) ) {
$val = unserialize( $val );
@@ -609,22 +513,22 @@ class eAccelBagOStuff extends BagOStuff {
return $val;
}
- function set($key, $value, $exptime=0) {
+ public function set( $key, $value, $exptime = 0 ) {
eaccelerator_put( $key, serialize( $value ), $exptime );
return true;
}
- function delete($key, $time=0) {
+ public function delete( $key, $time = 0 ) {
eaccelerator_rm( $key );
return true;
}
- function lock($key, $waitTimeout = 0 ) {
+ public function lock( $key, $waitTimeout = 0 ) {
eaccelerator_lock( $key );
return true;
}
- function unlock($key) {
+ public function unlock( $key ) {
eaccelerator_unlock( $key );
return true;
}
@@ -646,7 +550,7 @@ class XCacheBagOStuff extends BagOStuff {
*/
public function get( $key ) {
$val = xcache_get( $key );
- if( is_string( $val ) )
+ if ( is_string( $val ) )
$val = unserialize( $val );
return $val;
}
@@ -675,25 +579,29 @@ class XCacheBagOStuff extends BagOStuff {
xcache_unset( $key );
return true;
}
-
}
/**
- * @todo document
+ * Cache that uses DBA as a backend.
+ * Slow due to the need to constantly open and close the file to avoid holding
+ * writer locks. Intended for development use only, as a memcached workalike
+ * for systems that don't have it.
+ *
* @ingroup Cache
*/
class DBABagOStuff extends BagOStuff {
var $mHandler, $mFile, $mReader, $mWriter, $mDisabled;
- function __construct( $handler = 'db3', $dir = false ) {
+ public function __construct( $dir = false ) {
+ global $wgDBAhandler;
if ( $dir === false ) {
global $wgTmpDirectory;
$dir = $wgTmpDirectory;
}
$this->mFile = "$dir/mw-cache-" . wfWikiID();
$this->mFile .= '.db';
- wfDebug( __CLASS__.": using cache file {$this->mFile}\n" );
- $this->mHandler = $handler;
+ wfDebug( __CLASS__ . ": using cache file {$this->mFile}\n" );
+ $this->mHandler = $wgDBAhandler;
}
/**
@@ -701,7 +609,7 @@ class DBABagOStuff extends BagOStuff {
*/
function encode( $value, $expiry ) {
# Convert to absolute time
- $expiry = BagOStuff::convertExpiry( $expiry );
+ $expiry = $this->convertExpiry( $expiry );
return sprintf( '%010u', intval( $expiry ) ) . ' ' . serialize( $value );
}
@@ -715,7 +623,7 @@ class DBABagOStuff extends BagOStuff {
return array(
unserialize( substr( $blob, 11 ) ),
intval( substr( $blob, 0, 10 ) )
- );
+ );
}
}
@@ -741,7 +649,7 @@ class DBABagOStuff extends BagOStuff {
function get( $key ) {
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__."($key)\n" );
+ wfDebug( __METHOD__ . "($key)\n" );
$handle = $this->getReader();
if ( !$handle ) {
return null;
@@ -756,16 +664,16 @@ class DBABagOStuff extends BagOStuff {
$handle = $this->getWriter();
dba_delete( $key, $handle );
dba_close( $handle );
- wfDebug( __METHOD__.": $key expired\n" );
+ wfDebug( __METHOD__ . ": $key expired\n" );
$val = null;
}
wfProfileOut( __METHOD__ );
return $val;
}
- function set( $key, $value, $exptime=0 ) {
+ function set( $key, $value, $exptime = 0 ) {
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__."($key)\n" );
+ wfDebug( __METHOD__ . "($key)\n" );
$blob = $this->encode( $value, $exptime );
$handle = $this->getWriter();
if ( !$handle ) {
@@ -779,7 +687,7 @@ class DBABagOStuff extends BagOStuff {
function delete( $key, $time = 0 ) {
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__."($key)\n" );
+ wfDebug( __METHOD__ . "($key)\n" );
$handle = $this->getWriter();
if ( !$handle ) {
return false;
@@ -817,11 +725,11 @@ class DBABagOStuff extends BagOStuff {
function keys() {
$reader = $this->getReader();
$k1 = dba_firstkey( $reader );
- if( !$k1 ) {
+ if ( !$k1 ) {
return array();
}
$result[] = $k1;
- while( $key = dba_nextkey( $reader ) ) {
+ while ( $key = dba_nextkey( $reader ) ) {
$result[] = $key;
}
return $result;
diff --git a/includes/Block.php b/includes/Block.php
index a44941f1..187ff2db 100644
--- a/includes/Block.php
+++ b/includes/Block.php
@@ -34,7 +34,7 @@ class Block {
$this->mUser = $user;
$this->mBy = $by;
$this->mReason = $reason;
- $this->mTimestamp = wfTimestamp(TS_MW,$timestamp);
+ $this->mTimestamp = wfTimestamp( TS_MW, $timestamp );
$this->mAuto = $auto;
$this->mAnonOnly = $anonOnly;
$this->mCreateAccount = $createAccount;
@@ -54,7 +54,7 @@ class Block {
* Load a block from the database, using either the IP address or
* user ID. Tries the user ID first, and if that doesn't work, tries
* the address.
- *
+ *
* @param $address String: IP address of user/anon
* @param $user Integer: user id of user
* @param $killExpired Boolean: delete expired blocks on load
@@ -87,14 +87,14 @@ class Block {
return null;
}
}
-
+
/**
* Check if two blocks are effectively equal
*
* @return Boolean
*/
public function equals( Block $block ) {
- return (
+ return (
$this->mAddress == $block->mAddress
&& $this->mUser == $block->mUser
&& $this->mAuto == $block->mAuto
@@ -130,9 +130,10 @@ class Block {
*/
protected function &getDBOptions( &$options ) {
global $wgAntiLockFlags;
+
if ( $this->mForUpdate || $this->mFromMaster ) {
$db = wfGetDB( DB_MASTER );
- if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) {
+ if ( !$this->mForUpdate || ( $wgAntiLockFlags & ALF_NO_BLOCK_LOCK ) ) {
$options = array();
} else {
$options = array( 'FOR UPDATE' );
@@ -180,12 +181,13 @@ class Block {
if ( $address ) {
$conds = array( 'ipb_address' => $address, 'ipb_auto' => 0 );
$res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) );
+
if ( $this->loadFromResult( $res, $killExpired ) ) {
if ( $user && $this->mAnonOnly ) {
# Block is marked anon-only
# Whitelist this IP address against autoblocks and range blocks
# (but not account creation blocks -- bug 13611)
- if( !$this->mCreateAccount ) {
+ if ( !$this->mCreateAccount ) {
$this->clear();
}
return false;
@@ -199,7 +201,7 @@ class Block {
if ( $this->loadRange( $address, $killExpired, $user ) ) {
if ( $user && $this->mAnonOnly ) {
# Respect account creation blocks on logged-in users -- bug 13611
- if( !$this->mCreateAccount ) {
+ if ( !$this->mCreateAccount ) {
$this->clear();
}
return false;
@@ -211,10 +213,13 @@ class Block {
# Try autoblock
if ( $address ) {
$conds = array( 'ipb_address' => $address, 'ipb_auto' => 1 );
+
if ( $user ) {
$conds['ipb_anon_only'] = 0;
}
+
$res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) );
+
if ( $this->loadFromResult( $res, $killExpired ) ) {
return true;
}
@@ -234,6 +239,7 @@ class Block {
*/
protected function loadFromResult( ResultWrapper $res, $killExpired = true ) {
$ret = false;
+
if ( 0 != $res->numRows() ) {
# Get first block
$row = $res->fetchObject();
@@ -274,6 +280,7 @@ class Block {
*/
public function loadRange( $address, $killExpired = true, $user = 0 ) {
$iaddr = IP::toHex( $address );
+
if ( $iaddr === false ) {
# Invalid address
return false;
@@ -286,7 +293,7 @@ class Block {
$options = array();
$db =& $this->getDBOptions( $options );
$conds = array(
- "ipb_range_start LIKE '$range%'",
+ 'ipb_range_start' . $db->buildLike( $range, $db->anyString() ),
"ipb_range_start <= '$iaddr'",
"ipb_range_end >= '$iaddr'"
);
@@ -309,7 +316,7 @@ class Block {
public function initFromRow( $row ) {
$this->mAddress = $row->ipb_address;
$this->mReason = $row->ipb_reason;
- $this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp);
+ $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp );
$this->mUser = $row->ipb_user;
$this->mBy = $row->ipb_by;
$this->mAuto = $row->ipb_auto;
@@ -321,17 +328,19 @@ class Block {
$this->mHideName = $row->ipb_deleted;
$this->mId = $row->ipb_id;
$this->mExpiry = self::decodeExpiry( $row->ipb_expiry );
+
if ( isset( $row->user_name ) ) {
$this->mByName = $row->user_name;
} else {
$this->mByName = $row->ipb_by_text;
}
+
$this->mRangeStart = $row->ipb_range_start;
$this->mRangeEnd = $row->ipb_range_end;
}
/**
- * Once $mAddress has been set, get the range they came from.
+ * Once $mAddress has been set, get the range they came from.
* Wrapper for IP::parseRange
*/
protected function initialiseRange() {
@@ -352,6 +361,7 @@ class Block {
if ( wfReadOnly() ) {
return false;
}
+
if ( !$this->mId ) {
throw new MWException( "Block::delete() now requires that the mId member be filled\n" );
}
@@ -377,8 +387,9 @@ class Block {
# Don't collide with expired blocks
Block::purgeExpired();
- $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val');
- $dbw->insert( 'ipblocks',
+ $ipb_id = $dbw->nextSequenceValue( 'ipblocks_ipb_id_seq' );
+ $dbw->insert(
+ 'ipblocks',
array(
'ipb_id' => $ipb_id,
'ipb_address' => $this->mAddress,
@@ -386,7 +397,7 @@ class Block {
'ipb_by' => $this->mBy,
'ipb_by_text' => $this->mByName,
'ipb_reason' => $this->mReason,
- 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp),
+ 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ),
'ipb_auto' => $this->mAuto,
'ipb_anon_only' => $this->mAnonOnly,
'ipb_create_account' => $this->mCreateAccount,
@@ -394,14 +405,16 @@ class Block {
'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ),
'ipb_range_start' => $this->mRangeStart,
'ipb_range_end' => $this->mRangeEnd,
- 'ipb_deleted' => $this->mHideName,
+ 'ipb_deleted' => intval( $this->mHideName ), // typecast required for SQLite
'ipb_block_email' => $this->mBlockEmail,
'ipb_allow_usertalk' => $this->mAllowUsertalk
- ), 'Block::insert', array( 'IGNORE' )
+ ),
+ 'Block::insert',
+ array( 'IGNORE' )
);
$affected = $dbw->affectedRows();
- if ($affected)
+ if ( $affected )
$this->doRetroactiveAutoblock();
return (bool)$affected;
@@ -417,13 +430,14 @@ class Block {
$this->validateBlockParams();
- $dbw->update( 'ipblocks',
+ $dbw->update(
+ 'ipblocks',
array(
'ipb_user' => $this->mUser,
'ipb_by' => $this->mBy,
'ipb_by_text' => $this->mByName,
'ipb_reason' => $this->mReason,
- 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp),
+ 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ),
'ipb_auto' => $this->mAuto,
'ipb_anon_only' => $this->mAnonOnly,
'ipb_create_account' => $this->mCreateAccount,
@@ -433,13 +447,15 @@ class Block {
'ipb_range_end' => $this->mRangeEnd,
'ipb_deleted' => $this->mHideName,
'ipb_block_email' => $this->mBlockEmail,
- 'ipb_allow_usertalk' => $this->mAllowUsertalk ),
+ 'ipb_allow_usertalk' => $this->mAllowUsertalk
+ ),
array( 'ipb_id' => $this->mId ),
- 'Block::update' );
+ 'Block::update'
+ );
return $dbw->affectedRows();
}
-
+
/**
* Make sure all the proper members are set to sane values
* before adding/updating a block
@@ -453,11 +469,14 @@ class Block {
# Unset ipb_enable_autoblock for IP blocks, makes no sense
if ( !$this->mUser ) {
$this->mEnableAutoblock = 0;
- $this->mBlockEmail = 0; //Same goes for email...
}
- if( !$this->mByName ) {
- if( $this->mBy ) {
+ # bug 18860: non-anon-only IP blocks should be allowed to block email
+ if ( !$this->mUser && $this->mAnonOnly ) {
+ $this->mBlockEmail = 0;
+ }
+ if ( !$this->mByName ) {
+ if ( $this->mBy ) {
$this->mByName = User::whoIs( $this->mBy );
} else {
global $wgUser;
@@ -465,28 +484,27 @@ class Block {
}
}
}
-
-
+
/**
- * Retroactively autoblocks the last IP used by the user (if it is a user)
- * blocked by this Block.
- *
- * @return Boolean: whether or not a retroactive autoblock was made.
- */
+ * Retroactively autoblocks the last IP used by the user (if it is a user)
+ * blocked by this Block.
+ *
+ * @return Boolean: whether or not a retroactive autoblock was made.
+ */
public function doRetroactiveAutoblock() {
$dbr = wfGetDB( DB_SLAVE );
- #If autoblock is enabled, autoblock the LAST IP used
+ # If autoblock is enabled, autoblock the LAST IP used
# - stolen shamelessly from CheckUser_body.php
- if ($this->mEnableAutoblock && $this->mUser) {
- wfDebug("Doing retroactive autoblocks for " . $this->mAddress . "\n");
-
+ 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) {
+
+ 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) ) );
+ $conds[] = 'rc_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( time() - ( 7 * 86400 ) ) );
$options['LIMIT'] = 5;
} else {
// Just the last IP used.
@@ -494,11 +512,11 @@ class Block {
}
$res = $dbr->select( 'recentchanges', array( 'rc_ip' ), $conds,
- __METHOD__ , $options);
+ __METHOD__ , $options );
if ( !$dbr->numRows( $res ) ) {
- #No results, don't autoblock anything
- wfDebug("No IP found to retroactively autoblock\n");
+ # No results, don't autoblock anything
+ wfDebug( "No IP found to retroactively autoblock\n" );
} else {
while ( $row = $dbr->fetchObject( $res ) ) {
if ( $row->rc_ip )
@@ -507,7 +525,7 @@ class Block {
}
}
}
-
+
/**
* Checks whether a given IP is on the autoblock whitelist.
*
@@ -516,7 +534,7 @@ class Block {
*/
public static function isWhitelistedFromAutoblocks( $ip ) {
global $wgMemc;
-
+
// Try to get the autoblock_whitelist from the cache, as it's faster
// than getting the msg raw and explode()'ing it.
$key = wfMemcKey( 'ipb', 'autoblock', 'whitelist' );
@@ -526,28 +544,28 @@ class Block {
$wgMemc->set( $key, $lines, 3600 * 24 );
}
- wfDebug("Checking the autoblock whitelist..\n");
+ wfDebug( "Checking the autoblock whitelist..\n" );
- foreach( $lines as $line ) {
+ foreach ( $lines as $line ) {
# List items only
if ( substr( $line, 0, 1 ) !== '*' ) {
continue;
}
- $wlEntry = substr($line, 1);
- $wlEntry = trim($wlEntry);
+ $wlEntry = substr( $line, 1 );
+ $wlEntry = trim( $wlEntry );
- wfDebug("Checking $ip against $wlEntry...");
+ wfDebug( "Checking $ip against $wlEntry..." );
# Is the IP in this range?
- if (IP::isInRange( $ip, $wlEntry )) {
- wfDebug(" IP $ip matches $wlEntry, not autoblocking\n");
+ if ( IP::isInRange( $ip, $wlEntry ) ) {
+ wfDebug( " IP $ip matches $wlEntry, not autoblocking\n" );
return true;
} else {
wfDebug( " No match\n" );
}
}
-
+
return false;
}
@@ -565,12 +583,12 @@ class Block {
}
# Check for presence on the autoblock whitelist
- if (Block::isWhitelistedFromAutoblocks($autoblockIP)) {
+ if ( Block::isWhitelistedFromAutoblocks( $autoblockIP ) ) {
return;
}
-
- ## Allow hooks to cancel the autoblock.
- if (!wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) )) {
+
+ # # Allow hooks to cancel the autoblock.
+ if ( !wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) ) ) {
wfDebug( "Autoblock aborted by hook.\n" );
return false;
}
@@ -582,8 +600,8 @@ class Block {
# If the user is already blocked. Then check if the autoblock would
# exceed the user block. If it would exceed, then do nothing, else
# prolong block time
- if ($this->mExpiry &&
- ($this->mExpiry < Block::getAutoblockExpiry($ipblock->mTimestamp))) {
+ if ( $this->mExpiry &&
+ ( $this->mExpiry < Block::getAutoblockExpiry( $ipblock->mTimestamp ) ) ) {
return;
}
# Just update the timestamp
@@ -610,8 +628,8 @@ class Block {
$ipblock->mAllowUsertalk = $this->mAllowUsertalk;
# If the user is already blocked with an expiry date, we don't
# want to pile on top of that!
- if($this->mExpiry) {
- $ipblock->mExpiry = min ( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp ));
+ if ( $this->mExpiry ) {
+ $ipblock->mExpiry = min( $this->mExpiry, Block::getAutoblockExpiry( $this->mTimestamp ) );
} else {
$ipblock->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp );
}
@@ -624,8 +642,7 @@ class Block {
* @return Boolean
*/
public function deleteIfExpired() {
- $fname = 'Block::deleteIfExpired';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
if ( $this->isExpired() ) {
wfDebug( "Block::deleteIfExpired() -- deleting\n" );
$this->delete();
@@ -634,7 +651,7 @@ class Block {
wfDebug( "Block::deleteIfExpired() -- not expired\n" );
$retVal = false;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $retVal;
}
@@ -660,7 +677,7 @@ class Block {
}
/**
- * Update the timestamp on autoblocks.
+ * Update the timestamp on autoblocks.
*/
public function updateTimestamp() {
if ( $this->mAuto ) {
@@ -670,8 +687,8 @@ class Block {
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'ipblocks',
array( /* SET */
- 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp),
- 'ipb_expiry' => $dbw->timestamp($this->mExpiry),
+ 'ipb_timestamp' => $dbw->timestamp( $this->mTimestamp ),
+ 'ipb_expiry' => $dbw->timestamp( $this->mExpiry ),
), array( /* WHERE */
'ipb_address' => $this->mAddress
), 'Block::updateTimestamp'
@@ -700,14 +717,14 @@ class Block {
/**
* Get/set the SELECT ... FOR UPDATE flag
*/
- public function forUpdate( $x = NULL ) {
+ public function forUpdate( $x = null ) {
return wfSetVar( $this->mForUpdate, $x );
}
/**
* Get/set a flag determining whether the master is used for reads
*/
- public function fromMaster( $x = NULL ) {
+ public function fromMaster( $x = null ) {
return wfSetVar( $this->mFromMaster, $x );
}
@@ -726,7 +743,7 @@ class Block {
/**
* Encode expiry for DB
*
- * @param $expiry String: timestamp for expiry, or
+ * @param $expiry String: timestamp for expiry, or
* @param $db Database object
* @return String
*/
@@ -773,10 +790,10 @@ class Block {
$parts = explode( '/', $range );
if ( count( $parts ) == 2 ) {
// IPv6
- if ( IP::isIPv6($range) && $parts[1] >= 64 && $parts[1] <= 128 ) {
+ if ( IP::isIPv6( $range ) && $parts[1] >= 64 && $parts[1] <= 128 ) {
$bits = $parts[1];
$ipint = IP::toUnsigned6( $parts[0] );
- # Native 32 bit functions WONT work here!!!
+ # Native 32 bit functions WON'T work here!!!
# Convert to a padded binary number
$network = wfBaseConvert( $ipint, 10, 2, 128 );
# Truncate the last (128-$bits) bits and replace them with zeros
@@ -787,7 +804,7 @@ class Block {
$newip = IP::toOctet( $network );
$range = "$newip/{$parts[1]}";
} // IPv4
- else if ( IP::isIPv4($range) && $parts[1] >= 16 && $parts[1] <= 32 ) {
+ elseif ( IP::isIPv4( $range ) && $parts[1] >= 16 && $parts[1] <= 32 ) {
$shift = 32 - $parts[1];
$ipint = IP::toUnsigned( $parts[0] );
$ipint = $ipint >> $shift << $shift;
@@ -808,7 +825,7 @@ class Block {
/**
* Get a value to insert into expiry field of the database when infinite expiry
- * is desired. In principle this could be DBMS-dependant, but currently all
+ * is desired. In principle this could be DBMS-dependant, but currently all
* supported DBMS's support the string "infinity", so we just use that.
*
* @return String
@@ -818,35 +835,36 @@ class Block {
# works with CHAR(14) as well because "i" sorts after all numbers.
return 'infinity';
}
-
+
/**
* Convert a DB-encoded expiry into a real string that humans can read.
*
* @param $encoded_expiry String: Database encoded expiry time
- * @return String
+ * @return Html-escaped String
*/
public static function formatExpiry( $encoded_expiry ) {
static $msg = null;
-
- if( is_null( $msg ) ) {
+
+ if ( is_null( $msg ) ) {
$msg = array();
$keys = array( 'infiniteblock', 'expiringblock' );
- foreach( $keys as $key ) {
+ foreach ( $keys as $key ) {
$msg[$key] = wfMsgHtml( $key );
}
}
-
+
$expiry = Block::decodeExpiry( $encoded_expiry );
- if ($expiry == 'infinity') {
+ if ( $expiry == 'infinity' ) {
$expirystr = $msg['infiniteblock'];
} else {
global $wgLang;
- $expiretimestr = $wgLang->timeanddate( $expiry, true );
- $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array($expiretimestr) );
+ $expiredatestr = htmlspecialchars( $wgLang->date( $expiry, true ) );
+ $expiretimestr = htmlspecialchars( $wgLang->time( $expiry, true ) );
+ $expirystr = wfMsgReplaceArgs( $msg['expiringblock'], array( $expiredatestr, $expiretimestr ) );
}
return $expirystr;
}
-
+
/**
* Convert a typed-in expiry time into something we can put into the database.
* @param $expiry_input String: whatever was typed into the form
@@ -857,7 +875,8 @@ class Block {
$expiry = 'infinity';
} else {
$expiry = strtotime( $expiry_input );
- if ($expiry < 0 || $expiry === false) {
+
+ if ( $expiry < 0 || $expiry === false ) {
return false;
}
}
diff --git a/includes/CacheDependency.php b/includes/CacheDependency.php
index b050c46d..11e70738 100644
--- a/includes/CacheDependency.php
+++ b/includes/CacheDependency.php
@@ -1,5 +1,4 @@
<?php
-
/**
* This class stores an arbitrary value along with its dependencies.
* Users should typically only use DependencyWrapper::getFromCache(), rather
@@ -12,8 +11,8 @@ class DependencyWrapper {
/**
* Create an instance.
- * @param mixed $value The user-supplied value
- * @param mixed $deps A dependency or dependency array. All dependencies
+ * @param $value Mixed: the user-supplied value
+ * @param $deps Mixed: a dependency or dependency array. All dependencies
* must be objects implementing CacheDependency.
*/
function __construct( $value = false, $deps = array() ) {
@@ -66,12 +65,12 @@ class DependencyWrapper {
* it will be generated with the callback function (if present), and the newly
* 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 $cache Object: a cache object such as $wgMemc
+ * @param $key String: the cache key
+ * @param $expiry Integer: the expiry timestamp or interval in seconds
+ * @param $callback Mixed: the callback for generating the value, or false
+ * @param $callbackParams Array: the function parameters for the callback
+ * @param $deps Array: 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.
*
@@ -108,7 +107,7 @@ abstract class CacheDependency {
/**
* Hook to perform any expensive pre-serialize loading of dependency values.
*/
- function loadDependencyValues() {}
+ function loadDependencyValues() { }
}
/**
@@ -120,8 +119,8 @@ class FileDependency extends CacheDependency {
/**
* Create a file dependency
*
- * @param string $filename The name of the file, preferably fully qualified
- * @param mixed $timestamp The unix last modified timestamp, or false if the
+ * @param $filename String: the name of the file, preferably fully qualified
+ * @param $timestamp Mixed: the unix last modified timestamp, or false if the
* file does not exist. If omitted, the timestamp will be loaded from
* the file.
*
@@ -134,6 +133,11 @@ class FileDependency extends CacheDependency {
$this->timestamp = $timestamp;
}
+ function __sleep() {
+ $this->loadDependencyValues();
+ return array( 'filename', 'timestamp' );
+ }
+
function loadDependencyValues() {
if ( is_null( $this->timestamp ) ) {
if ( !file_exists( $this->filename ) ) {
@@ -180,7 +184,7 @@ class TitleDependency extends CacheDependency {
/**
* Construct a title dependency
- * @param Title $title
+ * @param $title Title
*/
function __construct( Title $title ) {
$this->titleObj = $title;
@@ -208,6 +212,7 @@ class TitleDependency extends CacheDependency {
function isExpired() {
$touched = $this->getTitle()->getTouched();
+
if ( $this->touched === false ) {
if ( $touched === false ) {
# Still missing
@@ -246,6 +251,7 @@ class TitleListDependency extends CacheDependency {
function calculateTimestamps() {
# Initialise values to false
$timestamps = array();
+
foreach ( $this->getLinkBatch()->data as $ns => $dbks ) {
if ( count( $dbks ) > 0 ) {
$timestamps[$ns] = array();
@@ -259,9 +265,13 @@ 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__ );
+ $where,
+ __METHOD__
+ );
+
while ( $row = $dbr->fetchObject( $res ) ) {
$timestamps[$row->page_namespace][$row->page_title] = $row->page_touched;
}
@@ -278,7 +288,7 @@ class TitleListDependency extends CacheDependency {
}
function getLinkBatch() {
- if ( !isset( $this->linkBatch ) ){
+ if ( !isset( $this->linkBatch ) ) {
$this->linkBatch = new LinkBatch;
$this->linkBatch->setArray( $this->timestamps );
}
@@ -290,6 +300,7 @@ class TitleListDependency extends CacheDependency {
foreach ( $this->timestamps as $ns => $dbks ) {
foreach ( $dbks as $dbk => $oldTimestamp ) {
$newTimestamp = $newTimestamps[$ns][$dbk];
+
if ( $oldTimestamp === false ) {
if ( $newTimestamp === false ) {
# Still missing
diff --git a/includes/Category.php b/includes/Category.php
index 50efdbc1..e9ffaecf 100644
--- a/includes/Category.php
+++ b/includes/Category.php
@@ -1,6 +1,6 @@
<?php
/**
- * Category objects are immutable, strictly speaking. If you call methods that change the database,
+ * 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.
*
@@ -18,21 +18,21 @@ class Category {
/** Counts of membership (cat_pages, cat_subcats, cat_files) */
private $mPages = null, $mSubcats = null, $mFiles = null;
- private function __construct() {}
+ 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->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 ) {
+ 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 ) {
+ } elseif ( $this->mName === null ) {
$where = array( 'cat_id' => $this->mID );
} else {
# Already initialized
@@ -45,12 +45,13 @@ class Category {
$where,
__METHOD__
);
- if( !$row ) {
+
+ 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->mName = $this->mTitle->getDBkey();
$this->mPages = 0;
$this->mSubcats = 0;
$this->mFiles = 0;
@@ -60,6 +61,7 @@ class Category {
return false; # Fail
}
}
+
$this->mID = $row->cat_id;
$this->mName = $row->cat_title;
$this->mPages = $row->cat_pages;
@@ -69,7 +71,7 @@ class Category {
# (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 ) {
+ if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
$this->refreshCounts();
}
@@ -86,12 +88,13 @@ class Category {
public static function newFromName( $name ) {
$cat = new self();
$title = Title::makeTitleSafe( NS_CATEGORY, $name );
- if( !is_object( $title ) ) {
+
+ if ( !is_object( $title ) ) {
return false;
}
$cat->mTitle = $title;
- $cat->mName = $title->getDBKey();
+ $cat->mName = $title->getDBkey();
return $cat;
}
@@ -106,7 +109,7 @@ class Category {
$cat = new self();
$cat->mTitle = $title;
- $cat->mName = $title->getDBKey();
+ $cat->mName = $title->getDBkey();
return $cat;
}
@@ -126,7 +129,7 @@ class Category {
/**
* 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,
+ * @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.
@@ -137,8 +140,7 @@ class Category {
$cat = new self();
$cat->mTitle = $title;
-
- # NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
+ # 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.
@@ -149,7 +151,7 @@ class Category {
# 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->mName = $title->getDBkey(); # if we have a title object, fetch the category name from there
}
$cat->mID = false;
@@ -169,12 +171,16 @@ class Category {
/** @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' ); }
@@ -182,9 +188,9 @@ class Category {
* @return mixed The Title for this category, or false on failure.
*/
public function getTitle() {
- if( $this->mTitle ) return $this->mTitle;
-
- if( !$this->initialize() ) {
+ if ( $this->mTitle ) return $this->mTitle;
+
+ if ( !$this->initialize() ) {
return false;
}
@@ -204,13 +210,19 @@ class Category {
$conds = array( 'cl_to' => $this->getName(), 'cl_from = page_id' );
$options = array( 'ORDER BY' => 'cl_sortkey' );
- if( $limit ) $options[ 'LIMIT' ] = $limit;
- if( $offset !== '' ) $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
+
+ if ( $limit ) {
+ $options[ 'LIMIT' ] = $limit;
+ }
+
+ if ( $offset !== '' ) {
+ $conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
+ }
return TitleArray::newFromResult(
$dbr->select(
array( 'page', 'categorylinks' ),
- array( 'page_id', 'page_namespace','page_title', 'page_len',
+ array( 'page_id', 'page_namespace', 'page_title', 'page_len',
'page_is_redirect', 'page_latest' ),
$conds,
__METHOD__,
@@ -221,10 +233,10 @@ class Category {
/** Generic accessor */
private function getX( $key ) {
- if( !$this->initialize() ) {
+ if ( !$this->initialize() ) {
return false;
}
- return $this->{$key};
+ return $this-> { $key } ;
}
/**
@@ -233,29 +245,33 @@ class Category {
* @return bool True on success, false on failure
*/
public function refreshCounts() {
- if( wfReadOnly() ) {
+ 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() ) {
+ 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!
+ $seqVal = $dbw->nextSequenceValue( 'category_cat_id_seq' );
$dbw->insert(
'category',
- array( 'cat_title' => $this->mName ),
+ array(
+ 'cat_id' => $seqVal,
+ 'cat_title' => $this->mName
+ ),
__METHOD__,
'IGNORE'
);
}
- $cond1 = $dbw->conditional( 'page_namespace='.NS_CATEGORY, 1, 'NULL' );
- $cond2 = $dbw->conditional( 'page_namespace='.NS_FILE, 1, 'NULL' );
+ $cond1 = $dbw->conditional( 'page_namespace=' . NS_CATEGORY, 1, 'NULL' );
+ $cond2 = $dbw->conditional( 'page_namespace=' . NS_FILE, 1, 'NULL' );
$result = $dbw->selectRow(
array( 'categorylinks', 'page' ),
array( 'COUNT(*) AS pages',
diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php
index 03ecb5dc..56f85faa 100644
--- a/includes/CategoryPage.php
+++ b/includes/CategoryPage.php
@@ -5,7 +5,7 @@
*
*/
-if( !defined( 'MEDIAWIKI' ) )
+if ( !defined( 'MEDIAWIKI' ) )
die( 1 );
/**
@@ -20,7 +20,7 @@ class CategoryPage extends Article {
if ( isset( $diff ) && $diffOnly )
return Article::view();
- if( !wfRunHooks( 'CategoryPageView', array( &$this ) ) )
+ if ( !wfRunHooks( 'CategoryPageView', array( &$this ) ) )
return;
if ( NS_CATEGORY == $this->mTitle->getNamespace() ) {
@@ -33,18 +33,17 @@ class CategoryPage extends Article {
$this->closeShowCategory();
}
}
-
+
/**
* Don't return a 404 for categories in use.
*/
function hasViewableContent() {
- if( parent::hasViewableContent() ) {
+ if ( parent::hasViewableContent() ) {
return true;
} else {
$cat = Category::newFromTitle( $this->mTitle );
return $cat->getId() != 0;
}
-
}
function openShowCategory() {
@@ -86,7 +85,7 @@ class CategoryViewer {
* @private
*/
function getHTML() {
- global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit;
+ global $wgOut, $wgCategoryMagicGallery, $wgCategoryPagingLimit, $wgContLang;
wfProfileIn( __METHOD__ );
$this->showGallery = $wgCategoryMagicGallery && !$wgOut->mNoGallery;
@@ -95,11 +94,22 @@ class CategoryViewer {
$this->doCategoryQuery();
$this->finaliseCategoryState();
- $r = $this->getCategoryTop() .
- $this->getSubcategorySection() .
+ $r = $this->getSubcategorySection() .
$this->getPagesSection() .
- $this->getImageSection() .
- $this->getCategoryBottom();
+ $this->getImageSection();
+
+ if ( $r == '' ) {
+ // If there is no category content to display, only
+ // show the top part of the navigation links.
+ // FIXME: cannot be completely suppressed because it
+ // is unknown if 'until' or 'from' makes this
+ // give 0 results.
+ $r = $r . $this->getCategoryTop();
+ } else {
+ $r = $this->getCategoryTop() .
+ $r .
+ $this->getCategoryBottom();
+ }
// Give a proper message if category is empty
if ( $r == '' ) {
@@ -107,7 +117,7 @@ class CategoryViewer {
}
wfProfileOut( __METHOD__ );
- return $r;
+ return $wgContLang->convert( $r );
}
function clearCategoryState() {
@@ -115,7 +125,7 @@ class CategoryViewer {
$this->articles_start_char = array();
$this->children = array();
$this->children_start_char = array();
- if( $this->showGallery ) {
+ if ( $this->showGallery ) {
$this->gallery = new ImageGallery();
$this->gallery->setHideBadImages();
}
@@ -138,14 +148,18 @@ class CategoryViewer {
}
/**
- * Add a subcategory to the internal lists, using a title object
+ * Add a subcategory to the internal lists, using a title object
* @deprecated 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(
- $title, $wgContLang->convertHtml( $title->getText() ) );
+ $this->children[] = $this->getSkin()->link(
+ $title,
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
$this->children_start_char[] = $this->getSubcategorySortChar( $title, $sortkey );
}
@@ -160,7 +174,7 @@ class CategoryViewer {
function getSubcategorySortChar( $title, $sortkey ) {
global $wgContLang;
- if( $title->getPrefixedText() == $sortkey ) {
+ if ( $title->getPrefixedText() == $sortkey ) {
$firstChar = $wgContLang->firstChar( $title->getDBkey() );
} else {
$firstChar = $wgContLang->firstChar( $sortkey );
@@ -174,7 +188,7 @@ class CategoryViewer {
*/
function addImage( Title $title, $sortkey, $pageLength, $isRedirect = false ) {
if ( $this->showGallery ) {
- if( $this->flip ) {
+ if ( $this->flip ) {
$this->gallery->insert( $title );
} else {
$this->gallery->add( $title );
@@ -189,15 +203,21 @@ class CategoryViewer {
*/
function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) {
global $wgContLang;
- $titletext = $wgContLang->convert( $title->getPrefixedText() );
$this->articles[] = $isRedirect
- ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title, $titletext ) . '</span>'
- : $this->getSkin()->makeSizeLinkObj( $pageLength, $title, $titletext );
+ ? '<span class="redirect-in-category">' .
+ $this->getSkin()->link(
+ $title,
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) . '</span>'
+ : $this->getSkin()->makeSizeLinkObj( $pageLength, $title );
$this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) );
}
function finaliseCategoryState() {
- if( $this->flip ) {
+ if ( $this->flip ) {
$this->children = array_reverse( $this->children );
$this->children_start_char = array_reverse( $this->children_start_char );
$this->articles = array_reverse( $this->articles );
@@ -207,16 +227,17 @@ class CategoryViewer {
function doCategoryQuery() {
$dbr = wfGetDB( DB_SLAVE, 'category' );
- if( $this->from != '' ) {
+ if ( $this->from != '' ) {
$pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from );
$this->flip = false;
- } elseif( $this->until != '' ) {
+ } elseif ( $this->until != '' ) {
$pageCondition = 'cl_sortkey < ' . $dbr->addQuotes( $this->until );
$this->flip = true;
} else {
$pageCondition = '1 = 1';
$this->flip = false;
}
+
$res = $dbr->select(
array( 'page', 'categorylinks', 'category' ),
array( 'page_title', 'page_namespace', 'page_len', 'page_is_redirect', 'cl_sortkey',
@@ -232,8 +253,9 @@ class CategoryViewer {
$count = 0;
$this->nextPage = null;
- while( $x = $dbr->fetchObject ( $res ) ) {
- if( ++$count > $this->limit ) {
+
+ while ( $x = $dbr->fetchObject ( $res ) ) {
+ if ( ++$count > $this->limit ) {
// We've reached the one extra which shows that there are
// additional pages to be had. Stop here...
$this->nextPage = $x->cl_sortkey;
@@ -242,26 +264,20 @@ class CategoryViewer {
$title = Title::makeTitle( $x->page_namespace, $x->page_title );
- if( $title->getNamespace() == NS_CATEGORY ) {
+ if ( $title->getNamespace() == NS_CATEGORY ) {
$cat = Category::newFromRow( $x, $title );
$this->addSubcategoryObject( $cat, $x->cl_sortkey, $x->page_len );
- } elseif( $this->showGallery && $title->getNamespace() == NS_FILE ) {
+ } elseif ( $this->showGallery && $title->getNamespace() == NS_FILE ) {
$this->addImage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
} else {
$this->addPage( $title, $x->cl_sortkey, $x->page_len, $x->page_is_redirect );
}
}
- $dbr->freeResult( $res );
}
function getCategoryTop() {
- $r = '';
- if( $this->until != '' ) {
- $r .= $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
- } elseif( $this->nextPage != '' || $this->from != '' ) {
- $r .= $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
- }
- return $r == ''
+ $r = $this->getCategoryBottom();
+ return $r === ''
? $r
: "<br style=\"clear:both;\"/>\n" . $r;
}
@@ -272,7 +288,8 @@ class CategoryViewer {
$rescnt = count( $this->children );
$dbcnt = $this->cat->getSubcatCount();
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
- if( $rescnt > 0 ) {
+
+ if ( $rescnt > 0 ) {
# Showing subcategories
$r .= "<div id=\"mw-subcategories\">\n";
$r .= '<h2>' . wfMsg( 'subcategories' ) . "</h2>\n";
@@ -297,7 +314,7 @@ class CategoryViewer {
$rescnt = count( $this->articles );
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
- if( $rescnt > 0 ) {
+ if ( $rescnt > 0 ) {
$r = "<div id=\"mw-pages\">\n";
$r .= '<h2>' . wfMsg( 'category_header', $ti ) . "</h2>\n";
$r .= $countmsg;
@@ -308,7 +325,7 @@ class CategoryViewer {
}
function getImageSection() {
- if( $this->showGallery && ! $this->gallery->isEmpty() ) {
+ if ( $this->showGallery && ! $this->gallery->isEmpty() ) {
$dbcnt = $this->cat->getFileCount();
$rescnt = $this->gallery->count();
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
@@ -322,9 +339,9 @@ class CategoryViewer {
}
function getCategoryBottom() {
- if( $this->until != '' ) {
+ if ( $this->until != '' ) {
return $this->pagingLinks( $this->title, $this->nextPage, $this->until, $this->limit );
- } elseif( $this->nextPage != '' || $this->from != '' ) {
+ } elseif ( $this->nextPage != '' || $this->from != '' ) {
return $this->pagingLinks( $this->title, $this->from, $this->nextPage, $this->limit );
} else {
return '';
@@ -344,7 +361,7 @@ class CategoryViewer {
function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
if ( count ( $articles ) > $cutoff ) {
return $this->columnList( $articles, $articles_start_char );
- } elseif ( count($articles) > 0) {
+ } elseif ( count( $articles ) > 0 ) {
// for short lists of articles in categories.
return $this->shortList( $articles, $articles_start_char );
}
@@ -355,61 +372,60 @@ class CategoryViewer {
* Format a list of articles chunked by letter in a three-column
* list, ordered vertically.
*
+ * TODO: Take the headers into account when creating columns, so they're
+ * more visually equal.
+ *
+ * More distant TODO: Scrap this and use CSS columns, whenever IE finally
+ * supports those.
+ *
* @param $articles Array
* @param $articles_start_char Array
* @return String
* @private
*/
function columnList( $articles, $articles_start_char ) {
- // divide list into three equal chunks
- $chunk = (int) (count ( $articles ) / 3);
+ $columns = array_combine( $articles, $articles_start_char );
+ # Split into three columns
+ $columns = array_chunk( $columns, ceil( count( $columns ) / 3 ), true /* preserve keys */ );
- // get and display header
- $r = '<table width="100%"><tr valign="top">';
+ $ret = '<table width="100%"><tr valign="top"><td>';
+ $prevchar = null;
- $prev_start_char = 'none';
+ foreach ( $columns as $column ) {
+ $colContents = array();
- // loop through the chunks
- for($startChunk = 0, $endChunk = $chunk, $chunkIndex = 0;
- $chunkIndex < 3;
- $chunkIndex++, $startChunk = $endChunk, $endChunk += $chunk + 1)
- {
- $r .= "<td>\n";
- $atColumnTop = true;
+ # Kind of like array_flip() here, but we keep duplicates in an
+ # array instead of dropping them.
+ foreach ( $column as $article => $char ) {
+ if ( !isset( $colContents[$char] ) ) {
+ $colContents[$char] = array();
+ }
+ $colContents[$char][] = $article;
+ }
- // output all articles in category
- for ($index = $startChunk ;
- $index < $endChunk && $index < count($articles);
- $index++ )
- {
- // check for change of starting letter or begining of chunk
- if ( ($index == $startChunk) ||
- ($articles_start_char[$index] != $articles_start_char[$index - 1]) )
-
- {
- if( $atColumnTop ) {
- $atColumnTop = false;
- } else {
- $r .= "</ul>\n";
- }
- $cont_msg = "";
- if ( $articles_start_char[$index] == $prev_start_char )
- $cont_msg = ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
- $r .= "<h3>" . htmlspecialchars( $articles_start_char[$index] ) . "$cont_msg</h3>\n<ul>";
- $prev_start_char = $articles_start_char[$index];
+ $first = true;
+ foreach ( $colContents as $char => $articles ) {
+ $ret .= '<h3>' . htmlspecialchars( $char );
+ if ( $first && $char === $prevchar ) {
+ # We're continuing a previous chunk at the top of a new
+ # column, so add " cont." after the letter.
+ $ret .= ' ' . wfMsgHtml( 'listingcontinuesabbrev' );
}
+ $ret .= "</h3>\n";
- $r .= "<li>{$articles[$index]}</li>";
- }
- if( !$atColumnTop ) {
- $r .= "</ul>\n";
- }
- $r .= "</td>\n";
+ $ret .= '<ul><li>';
+ $ret .= implode( "</li>\n<li>", $articles );
+ $ret .= '</li></ul>';
+ $first = false;
+ $prevchar = $char;
+ }
+ $ret .= "</td>\n<td>";
}
- $r .= '</tr></table>';
- return $r;
+
+ $ret .= '</td></tr></table>';
+ return $ret;
}
/**
@@ -421,10 +437,10 @@ class CategoryViewer {
*/
function shortList( $articles, $articles_start_char ) {
$r = '<h3>' . htmlspecialchars( $articles_start_char[0] ) . "</h3>\n";
- $r .= '<ul><li>'.$articles[0].'</li>';
- for ($index = 1; $index < count($articles); $index++ )
+ $r .= '<ul><li>' . $articles[0] . '</li>';
+ for ( $index = 1; $index < count( $articles ); $index++ )
{
- if ($articles_start_char[$index] != $articles_start_char[$index - 1])
+ if ( $articles_start_char[$index] != $articles_start_char[$index - 1] )
{
$r .= "</ul><h3>" . htmlspecialchars( $articles_start_char[$index] ) . "</h3>\n<ul>";
}
@@ -450,14 +466,29 @@ class CategoryViewer {
$limitText = $wgLang->formatNum( $limit );
$prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText );
- if( $first != '' ) {
- $prevLink = $sk->makeLinkObj( $title, $prevLink,
- wfArrayToCGI( $query + array( 'until' => $first ) ) );
+
+ if ( $first != '' ) {
+ $prevQuery = $query;
+ $prevQuery['until'] = $first;
+ $prevLink = $sk->linkKnown(
+ $title,
+ $prevLink,
+ array(),
+ $prevQuery
+ );
}
+
$nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText );
- if( $last != '' ) {
- $nextLink = $sk->makeLinkObj( $title, $nextLink,
- wfArrayToCGI( $query + array( 'from' => $last ) ) );
+
+ if ( $last != '' ) {
+ $lastQuery = $query;
+ $lastQuery['from'] = $last;
+ $nextLink = $sk->linkKnown(
+ $title,
+ $nextLink,
+ array(),
+ $lastQuery
+ );
}
return "($prevLink) ($nextLink)";
@@ -490,12 +521,14 @@ class CategoryViewer {
# 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)){
+ ( $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){
+ } elseif ( $totalrescnt < $this->limit && !$this->from && !$this->until ) {
# Case 2: not sane, but salvageable. Use the number of results.
# Since there are fewer than 200, we can also take this opportunity
# to refresh the incorrect category table entry -- which should be
@@ -504,10 +537,14 @@ class CategoryViewer {
$this->cat->refreshCounts();
} else {
# Case 3: hopeless. Don't give a total count at all.
- return wfMsgExt("category-$type-count-limited", 'parse',
+ return wfMsgExt( "category-$type-count-limited", 'parse',
$wgLang->formatNum( $rescnt ) );
}
- return wfMsgExt( "category-$type-count", 'parse', $wgLang->formatNum( $rescnt ),
- $wgLang->formatNum( $totalcnt ) );
+ return wfMsgExt(
+ "category-$type-count",
+ 'parse',
+ $wgLang->formatNum( $rescnt ),
+ $wgLang->formatNum( $totalcnt )
+ );
}
}
diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php
index 7c1c2856..5ac8a9be 100644
--- a/includes/Categoryfinder.php
+++ b/includes/Categoryfinder.php
@@ -1,5 +1,4 @@
<?php
-
/**
* The "Categoryfinder" class takes a list of articles, creates an internal
* representation of all their parent categories (as well as parents of
@@ -23,15 +22,14 @@
*
*/
class Categoryfinder {
-
- var $articles = array () ; # The original article IDs passed to the seed function
- var $deadend = array () ; # Array of DBKEY category names for categories that don't have a page
- var $parents = array () ; # Array of [ID => array()]
- var $next = array () ; # Array of article/category IDs
- var $targets = array () ; # Array of DBKEY category names
- var $name2id = array () ;
- var $mode ; # "AND" or "OR"
- var $dbr ; # Read-DB slave
+ var $articles = array(); # The original article IDs passed to the seed function
+ var $deadend = array(); # Array of DBKEY category names for categories that don't have a page
+ var $parents = array(); # Array of [ID => array()]
+ var $next = array(); # Array of article/category IDs
+ var $targets = array(); # Array of DBKEY category names
+ var $name2id = array();
+ var $mode; # "AND" or "OR"
+ var $dbr; # Read-DB slave
/**
* Constructor (currently empty).
@@ -45,16 +43,16 @@ class Categoryfinder {
* @param $categories FIXME
* @param $mode String: FIXME, default 'AND'.
*/
- function seed ( $article_ids , $categories , $mode = "AND" ) {
- $this->articles = $article_ids ;
- $this->next = $article_ids ;
- $this->mode = $mode ;
+ function seed( $article_ids, $categories, $mode = "AND" ) {
+ $this->articles = $article_ids;
+ $this->next = $article_ids;
+ $this->mode = $mode;
# Set the list of target categories; convert them to DBKEY form first
- $this->targets = array () ;
- foreach ( $categories AS $c ) {
+ $this->targets = array();
+ foreach ( $categories as $c ) {
$ct = Title::makeTitleSafe( NS_CATEGORY, $c );
- if( $ct ) {
+ if ( $ct ) {
$c = $ct->getDBkey();
$this->targets[$c] = $c;
}
@@ -69,19 +67,20 @@ class Categoryfinder {
function run () {
$this->dbr = wfGetDB( DB_SLAVE );
while ( count ( $this->next ) > 0 ) {
- $this->scan_next_layer () ;
+ $this->scan_next_layer();
}
# Now check if this applies to the individual articles
- $ret = array () ;
- foreach ( $this->articles AS $article ) {
- $conds = $this->targets ;
- if ( $this->check ( $article , $conds ) ) {
+ $ret = array();
+
+ foreach ( $this->articles as $article ) {
+ $conds = $this->targets;
+ if ( $this->check( $article, $conds ) ) {
# Matches the conditions
- $ret[] = $article ;
+ $ret[] = $article;
}
}
- return $ret ;
+ return $ret;
}
/**
@@ -91,108 +90,111 @@ class Categoryfinder {
* @param $path used to check for recursion loops
* @return bool Does this match the conditions?
*/
- function check ( $id , &$conds, $path=array() ) {
+ function check( $id , &$conds, $path = array() ) {
// Check for loops and stop!
- if( in_array( $id, $path ) )
+ if ( in_array( $id, $path ) ) {
return false;
+ }
+
$path[] = $id;
# Shortcut (runtime paranoia): No contitions=all matched
- if ( count ( $conds ) == 0 ) return true ;
+ if ( count( $conds ) == 0 ) {
+ return true;
+ }
- if ( !isset ( $this->parents[$id] ) ) return false ;
+ if ( !isset( $this->parents[$id] ) ) {
+ return false;
+ }
# iterate through the parents
- foreach ( $this->parents[$id] AS $p ) {
+ foreach ( $this->parents[$id] as $p ) {
$pname = $p->cl_to ;
# Is this a condition?
- if ( isset ( $conds[$pname] ) ) {
+ if ( isset( $conds[$pname] ) ) {
# This key is in the category list!
if ( $this->mode == "OR" ) {
# One found, that's enough!
- $conds = array () ;
- return true ;
+ $conds = array();
+ return true;
} else {
# Assuming "AND" as default
- unset ( $conds[$pname] ) ;
- if ( count ( $conds ) == 0 ) {
+ unset( $conds[$pname] ) ;
+ if ( count( $conds ) == 0 ) {
# All conditions met, done
- return true ;
+ return true;
}
}
}
# Not done yet, try sub-parents
- if ( !isset ( $this->name2id[$pname] ) ) {
+ if ( !isset( $this->name2id[$pname] ) ) {
# No sub-parent
continue ;
}
- $done = $this->check ( $this->name2id[$pname] , $conds, $path );
- if ( $done OR count ( $conds ) == 0 ) {
+ $done = $this->check( $this->name2id[$pname], $conds, $path );
+ if ( $done || count( $conds ) == 0 ) {
# Subparents have done it!
- return true ;
+ return true;
}
}
- return false ;
+ return false;
}
/**
* Scans a "parent layer" of the articles/categories in $this->next
*/
- function scan_next_layer () {
- $fname = "Categoryfinder::scan_next_layer" ;
-
+ function scan_next_layer() {
# Find all parents of the article currently in $this->next
- $layer = array () ;
+ $layer = array();
$res = $this->dbr->select(
- /* FROM */ 'categorylinks',
- /* SELECT */ '*',
- /* WHERE */ array( 'cl_from' => $this->next ),
- $fname."-1"
+ /* FROM */ 'categorylinks',
+ /* SELECT */ '*',
+ /* WHERE */ array( 'cl_from' => $this->next ),
+ __METHOD__ . "-1"
);
while ( $o = $this->dbr->fetchObject( $res ) ) {
$k = $o->cl_to ;
# Update parent tree
- if ( !isset ( $this->parents[$o->cl_from] ) ) {
- $this->parents[$o->cl_from] = array () ;
+ if ( !isset( $this->parents[$o->cl_from] ) ) {
+ $this->parents[$o->cl_from] = array();
}
- $this->parents[$o->cl_from][$k] = $o ;
+ $this->parents[$o->cl_from][$k] = $o;
# Ignore those we already have
- if ( in_array ( $k , $this->deadend ) ) continue ;
- if ( isset ( $this->name2id[$k] ) ) continue ;
+ if ( in_array ( $k , $this->deadend ) ) continue;
+
+ if ( isset ( $this->name2id[$k] ) ) continue;
# Hey, new category!
- $layer[$k] = $k ;
+ $layer[$k] = $k;
}
- $this->dbr->freeResult( $res ) ;
- $this->next = array() ;
+ $this->next = array();
# Find the IDs of all category pages in $layer, if they exist
if ( count ( $layer ) > 0 ) {
$res = $this->dbr->select(
- /* FROM */ 'page',
- /* SELECT */ 'page_id,page_title',
- /* WHERE */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ),
- $fname."-2"
+ /* FROM */ 'page',
+ /* SELECT */ array( 'page_id', 'page_title' ),
+ /* WHERE */ array( 'page_namespace' => NS_CATEGORY , 'page_title' => $layer ),
+ __METHOD__ . "-2"
);
while ( $o = $this->dbr->fetchObject( $res ) ) {
- $id = $o->page_id ;
- $name = $o->page_title ;
- $this->name2id[$name] = $id ;
- $this->next[] = $id ;
- unset ( $layer[$name] ) ;
- }
- $this->dbr->freeResult( $res ) ;
+ $id = $o->page_id;
+ $name = $o->page_title;
+ $this->name2id[$name] = $id;
+ $this->next[] = $id;
+ unset( $layer[$name] );
}
+ }
# Mark dead ends
- foreach ( $layer AS $v ) {
- $this->deadend[$v] = $v ;
+ foreach ( $layer as $v ) {
+ $this->deadend[$v] = $v;
}
}
-} # END OF CLASS "Categoryfinder"
+}
diff --git a/includes/Cdb.php b/includes/Cdb.php
new file mode 100644
index 00000000..ab429872
--- /dev/null
+++ b/includes/Cdb.php
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * Read from a CDB file.
+ * Native and pure PHP implementations are provided.
+ * http://cr.yp.to/cdb.html
+ */
+abstract class CdbReader {
+ /**
+ * Open a file and return a subclass instance
+ */
+ public static function open( $fileName ) {
+ if ( self::haveExtension() ) {
+ return new CdbReader_DBA( $fileName );
+ } else {
+ wfDebug( "Warning: no dba extension found, using emulation.\n" );
+ return new CdbReader_PHP( $fileName );
+ }
+ }
+
+ /**
+ * Returns true if the native extension is available
+ */
+ public static function haveExtension() {
+ if ( !function_exists( 'dba_handlers' ) ) {
+ return false;
+ }
+ $handlers = dba_handlers();
+ if ( !in_array( 'cdb', $handlers ) || !in_array( 'cdb_make', $handlers ) ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Construct the object and open the file
+ */
+ abstract function __construct( $fileName );
+
+ /**
+ * Close the file. Optional, you can just let the variable go out of scope.
+ */
+ abstract function close();
+
+ /**
+ * Get a value with a given key. Only string values are supported.
+ */
+ abstract public function get( $key );
+}
+
+/**
+ * Write to a CDB file.
+ * Native and pure PHP implementations are provided.
+ */
+abstract class CdbWriter {
+ /**
+ * Open a writer and return a subclass instance.
+ * The user must have write access to the directory, for temporary file creation.
+ */
+ public static function open( $fileName ) {
+ if ( CdbReader::haveExtension() ) {
+ return new CdbWriter_DBA( $fileName );
+ } else {
+ wfDebug( "Warning: no dba extension found, using emulation.\n" );
+ return new CdbWriter_PHP( $fileName );
+ }
+ }
+
+ /**
+ * Create the object and open the file
+ */
+ abstract function __construct( $fileName );
+
+ /**
+ * Set a key to a given value. The value will be converted to string.
+ */
+ abstract public function set( $key, $value );
+
+ /**
+ * Close the writer object. You should call this function before the object
+ * goes out of scope, to write out the final hashtables.
+ */
+ abstract public function close();
+}
+
+
+/**
+ * Reader class which uses the DBA extension
+ */
+class CdbReader_DBA {
+ var $handle;
+
+ function __construct( $fileName ) {
+ $this->handle = dba_open( $fileName, 'r-', 'cdb' );
+ if ( !$this->handle ) {
+ throw new MWException( 'Unable to open DB file "' . $fileName . '"' );
+ }
+ }
+
+ function close() {
+ if( isset($this->handle) )
+ dba_close( $this->handle );
+ unset( $this->handle );
+ }
+
+ function get( $key ) {
+ return dba_fetch( $key, $this->handle );
+ }
+}
+
+
+/**
+ * Writer class which uses the DBA extension
+ */
+class CdbWriter_DBA {
+ var $handle, $realFileName, $tmpFileName;
+
+ function __construct( $fileName ) {
+ $this->realFileName = $fileName;
+ $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff );
+ $this->handle = dba_open( $this->tmpFileName, 'n', 'cdb_make' );
+ if ( !$this->handle ) {
+ throw new MWException( 'Unable to open DB file for write "' . $fileName . '"' );
+ }
+ }
+
+ function set( $key, $value ) {
+ return dba_insert( $key, $value, $this->handle );
+ }
+
+ function close() {
+ if( isset($this->handle) )
+ dba_close( $this->handle );
+ if ( wfIsWindows() ) {
+ unlink( $this->realFileName );
+ }
+ if ( !rename( $this->tmpFileName, $this->realFileName ) ) {
+ throw new MWException( 'Unable to move the new CDB file into place.' );
+ }
+ unset( $this->handle );
+ }
+
+ function __destruct() {
+ if ( isset( $this->handle ) ) {
+ $this->close();
+ }
+ }
+}
+
diff --git a/includes/Cdb_PHP.php b/includes/Cdb_PHP.php
new file mode 100644
index 00000000..49294f71
--- /dev/null
+++ b/includes/Cdb_PHP.php
@@ -0,0 +1,374 @@
+<?php
+
+/**
+ * This is a port of D.J. Bernstein's CDB to PHP. It's based on the copy that
+ * appears in PHP 5.3. Changes are:
+ * * Error returns replaced with exceptions
+ * * Exception thrown if sizes or offsets are between 2GB and 4GB
+ * * Some variables renamed
+ */
+
+/**
+ * Common functions for readers and writers
+ */
+class CdbFunctions {
+ /**
+ * Take a modulo of a signed integer as if it were an unsigned integer.
+ * $b must be less than 0x40000000 and greater than 0
+ */
+ public static function unsignedMod( $a, $b ) {
+ if ( $a & 0x80000000 ) {
+ $m = ( $a & 0x7fffffff ) % $b + 2 * ( 0x40000000 % $b );
+ return $m % $b;
+ } else {
+ return $a % $b;
+ }
+ }
+
+ /**
+ * Shift a signed integer right as if it were unsigned
+ */
+ public static function unsignedShiftRight( $a, $b ) {
+ if ( $b == 0 ) {
+ return $a;
+ }
+ if ( $a & 0x80000000 ) {
+ return ( ( $a & 0x7fffffff ) >> $b ) | ( 0x40000000 >> ( $b - 1 ) );
+ } else {
+ return $a >> $b;
+ }
+ }
+
+ /**
+ * The CDB hash function.
+ */
+ public static function hash( $s ) {
+ $h = 5381;
+ for ( $i = 0; $i < strlen( $s ); $i++ ) {
+ $h5 = ($h << 5) & 0xffffffff;
+ // Do a 32-bit sum
+ // Inlined here for speed
+ $sum = ($h & 0x3fffffff) + ($h5 & 0x3fffffff);
+ $h =
+ (
+ ( $sum & 0x40000000 ? 1 : 0 )
+ + ( $h & 0x80000000 ? 2 : 0 )
+ + ( $h & 0x40000000 ? 1 : 0 )
+ + ( $h5 & 0x80000000 ? 2 : 0 )
+ + ( $h5 & 0x40000000 ? 1 : 0 )
+ ) << 30
+ | ( $sum & 0x3fffffff );
+ $h ^= ord( $s[$i] );
+ $h &= 0xffffffff;
+ }
+ return $h;
+ }
+}
+
+/**
+ * CDB reader class
+ */
+class CdbReader_PHP extends CdbReader {
+ /** The file handle */
+ var $handle;
+
+ /* number of hash slots searched under this key */
+ var $loop;
+
+ /* initialized if loop is nonzero */
+ var $khash;
+
+ /* initialized if loop is nonzero */
+ var $kpos;
+
+ /* initialized if loop is nonzero */
+ var $hpos;
+
+ /* initialized if loop is nonzero */
+ var $hslots;
+
+ /* initialized if findNext() returns true */
+ var $dpos;
+
+ /* initialized if cdb_findnext() returns 1 */
+ var $dlen;
+
+ function __construct( $fileName ) {
+ $this->handle = fopen( $fileName, 'rb' );
+ if ( !$this->handle ) {
+ throw new MWException( 'Unable to open DB file "' . $fileName . '"' );
+ }
+ $this->findStart();
+ }
+
+ function close() {
+ if( isset($this->handle) )
+ fclose( $this->handle );
+ unset( $this->handle );
+ }
+
+ public function get( $key ) {
+ // strval is required
+ if ( $this->find( strval( $key ) ) ) {
+ return $this->read( $this->dlen, $this->dpos );
+ } else {
+ return false;
+ }
+ }
+
+ protected function match( $key, $pos ) {
+ $buf = $this->read( strlen( $key ), $pos );
+ return $buf === $key;
+ }
+
+ protected function findStart() {
+ $this->loop = 0;
+ }
+
+ protected function read( $length, $pos ) {
+ if ( fseek( $this->handle, $pos ) == -1 ) {
+ // This can easily happen if the internal pointers are incorrect
+ throw new MWException( __METHOD__.': seek failed, file may be corrupted.' );
+ }
+
+ if ( $length == 0 ) {
+ return '';
+ }
+
+ $buf = fread( $this->handle, $length );
+ if ( $buf === false || strlen( $buf ) !== $length ) {
+ throw new MWException( __METHOD__.': read from cdb file failed, file may be corrupted' );
+ }
+ return $buf;
+ }
+
+ /**
+ * Unpack an unsigned integer and throw an exception if it needs more than 31 bits
+ */
+ protected function unpack31( $s ) {
+ $data = unpack( 'V', $s );
+ if ( $data[1] > 0x7fffffff ) {
+ throw new MWException( __METHOD__.': error in CDB file, integer too big' );
+ }
+ return $data[1];
+ }
+
+ /**
+ * Unpack a 32-bit signed integer
+ */
+ protected function unpackSigned( $s ) {
+ $data = unpack( 'va/vb', $s );
+ return $data['a'] | ( $data['b'] << 16 );
+ }
+
+ protected function findNext( $key ) {
+ if ( !$this->loop ) {
+ $u = CdbFunctions::hash( $key );
+ $buf = $this->read( 8, ( $u << 3 ) & 2047 );
+ $this->hslots = $this->unpack31( substr( $buf, 4 ) );
+ if ( !$this->hslots ) {
+ return false;
+ }
+ $this->hpos = $this->unpack31( substr( $buf, 0, 4 ) );
+ $this->khash = $u;
+ $u = CdbFunctions::unsignedShiftRight( $u, 8 );
+ $u = CdbFunctions::unsignedMod( $u, $this->hslots );
+ $u <<= 3;
+ $this->kpos = $this->hpos + $u;
+ }
+
+ while ( $this->loop < $this->hslots ) {
+ $buf = $this->read( 8, $this->kpos );
+ $pos = $this->unpack31( substr( $buf, 4 ) );
+ if ( !$pos ) {
+ return false;
+ }
+ $this->loop += 1;
+ $this->kpos += 8;
+ if ( $this->kpos == $this->hpos + ( $this->hslots << 3 ) ) {
+ $this->kpos = $this->hpos;
+ }
+ $u = $this->unpackSigned( substr( $buf, 0, 4 ) );
+ if ( $u === $this->khash ) {
+ $buf = $this->read( 8, $pos );
+ $keyLen = $this->unpack31( substr( $buf, 0, 4 ) );
+ if ( $keyLen == strlen( $key ) && $this->match( $key, $pos + 8 ) ) {
+ // Found
+ $this->dlen = $this->unpack31( substr( $buf, 4 ) );
+ $this->dpos = $pos + 8 + $keyLen;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ protected function find( $key ) {
+ $this->findStart();
+ return $this->findNext( $key );
+ }
+}
+
+/**
+ * CDB writer class
+ */
+class CdbWriter_PHP extends CdbWriter {
+ var $handle, $realFileName, $tmpFileName;
+
+ var $hplist;
+ var $numEntries, $pos;
+
+ function __construct( $fileName ) {
+ $this->realFileName = $fileName;
+ $this->tmpFileName = $fileName . '.tmp.' . mt_rand( 0, 0x7fffffff );
+ $this->handle = fopen( $this->tmpFileName, 'wb' );
+ if ( !$this->handle ) {
+ throw new MWException( 'Unable to open DB file for write "' . $fileName . '"' );
+ }
+ $this->hplist = array();
+ $this->numentries = 0;
+ $this->pos = 2048; // leaving space for the pointer array, 256 * 8
+ if ( fseek( $this->handle, $this->pos ) == -1 ) {
+ throw new MWException( __METHOD__.': fseek failed' );
+ }
+ }
+
+ function __destruct() {
+ if ( isset( $this->handle ) ) {
+ $this->close();
+ }
+ }
+
+ public function set( $key, $value ) {
+ if ( strval( $key ) === '' ) {
+ // DBA cross-check hack
+ return;
+ }
+ $this->addbegin( strlen( $key ), strlen( $value ) );
+ $this->write( $key );
+ $this->write( $value );
+ $this->addend( strlen( $key ), strlen( $value ), CdbFunctions::hash( $key ) );
+ }
+
+ public function close() {
+ $this->finish();
+ if( isset($this->handle) )
+ fclose( $this->handle );
+ if ( wfIsWindows() && file_exists($this->realFileName) ) {
+ unlink( $this->realFileName );
+ }
+ if ( !rename( $this->tmpFileName, $this->realFileName ) ) {
+ throw new MWException( 'Unable to move the new CDB file into place.' );
+ }
+ unset( $this->handle );
+ }
+
+ protected function write( $buf ) {
+ $len = fwrite( $this->handle, $buf );
+ if ( $len !== strlen( $buf ) ) {
+ throw new MWException( 'Error writing to CDB file.' );
+ }
+ }
+
+ protected function posplus( $len ) {
+ $newpos = $this->pos + $len;
+ if ( $newpos > 0x7fffffff ) {
+ throw new MWException( 'A value in the CDB file is too large' );
+ }
+ $this->pos = $newpos;
+ }
+
+ protected function addend( $keylen, $datalen, $h ) {
+ $this->hplist[] = array(
+ 'h' => $h,
+ 'p' => $this->pos
+ );
+
+ $this->numentries++;
+ $this->posplus( 8 );
+ $this->posplus( $keylen );
+ $this->posplus( $datalen );
+ }
+
+ protected function addbegin( $keylen, $datalen ) {
+ if ( $keylen > 0x7fffffff ) {
+ throw new MWException( __METHOD__.': key length too long' );
+ }
+ if ( $datalen > 0x7fffffff ) {
+ throw new MWException( __METHOD__.': data length too long' );
+ }
+ $buf = pack( 'VV', $keylen, $datalen );
+ $this->write( $buf );
+ }
+
+ protected function finish() {
+ // Hack for DBA cross-check
+ $this->hplist = array_reverse( $this->hplist );
+
+ // Calculate the number of items that will be in each hashtable
+ $counts = array_fill( 0, 256, 0 );
+ foreach ( $this->hplist as $item ) {
+ ++ $counts[ 255 & $item['h'] ];
+ }
+
+ // Fill in $starts with the *end* indexes
+ $starts = array();
+ $pos = 0;
+ for ( $i = 0; $i < 256; ++$i ) {
+ $pos += $counts[$i];
+ $starts[$i] = $pos;
+ }
+
+ // Excessively clever and indulgent code to simultaneously fill $packedTables
+ // with the packed hashtables, and adjust the elements of $starts
+ // to actually point to the starts instead of the ends.
+ $packedTables = array_fill( 0, $this->numentries, false );
+ foreach ( $this->hplist as $item ) {
+ $packedTables[--$starts[255 & $item['h']]] = $item;
+ }
+
+ $final = '';
+ for ( $i = 0; $i < 256; ++$i ) {
+ $count = $counts[$i];
+
+ // The size of the hashtable will be double the item count.
+ // The rest of the slots will be empty.
+ $len = $count + $count;
+ $final .= pack( 'VV', $this->pos, $len );
+
+ $hashtable = array();
+ for ( $u = 0; $u < $len; ++$u ) {
+ $hashtable[$u] = array( 'h' => 0, 'p' => 0 );
+ }
+
+ // Fill the hashtable, using the next empty slot if the hashed slot
+ // is taken.
+ for ( $u = 0; $u < $count; ++$u ) {
+ $hp = $packedTables[$starts[$i] + $u];
+ $where = CdbFunctions::unsignedMod(
+ CdbFunctions::unsignedShiftRight( $hp['h'], 8 ), $len );
+ while ( $hashtable[$where]['p'] )
+ if ( ++$where == $len )
+ $where = 0;
+ $hashtable[$where] = $hp;
+ }
+
+ // Write the hashtable
+ for ( $u = 0; $u < $len; ++$u ) {
+ $buf = pack( 'vvV',
+ $hashtable[$u]['h'] & 0xffff,
+ CdbFunctions::unsignedShiftRight( $hashtable[$u]['h'], 16 ),
+ $hashtable[$u]['p'] );
+ $this->write( $buf );
+ $this->posplus( 8 );
+ }
+ }
+
+ // Write the pointer array at the start of the file
+ rewind( $this->handle );
+ if ( ftell( $this->handle ) != 0 ) {
+ throw new MWException( __METHOD__.': Error rewinding to start of file' );
+ }
+ $this->write( $final );
+ }
+}
diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php
index de804c5c..8dce679b 100644
--- a/includes/ChangeTags.php
+++ b/includes/ChangeTags.php
@@ -1,56 +1,63 @@
<?php
-if (!defined( 'MEDIAWIKI' ))
+if( !defined( 'MEDIAWIKI' ) )
die;
class ChangeTags {
static function formatSummaryRow( $tags, $page ) {
- if (!$tags)
- return array('',array());
+ if( !$tags )
+ return array( '', array() );
$classes = array();
-
+
$tags = explode( ',', $tags );
$displayTags = array();
foreach( $tags as $tag ) {
- $displayTags[] = self::tagDescription( $tag );
- $classes[] = "mw-tag-$tag";
+ $displayTags[] = Xml::tags(
+ 'span',
+ array( 'class' => 'mw-tag-marker ' .
+ Sanitizer::escapeClass( "mw-tag-marker-$tag" ) ),
+ self::tagDescription( $tag )
+ );
+ $classes[] = Sanitizer::escapeClass( "mw-tag-$tag" );
}
- return array( '(' . implode( ', ', $displayTags ) . ')', $classes );
+ $markers = '(' . implode( ', ', $displayTags ) . ')';
+ $markers = Xml::tags( 'span', array( 'class' => 'mw-tag-markers' ), $markers );
+ return array( $markers, $classes );
}
static function tagDescription( $tag ) {
$msg = wfMsgExt( "tag-$tag", 'parseinline' );
if ( wfEmptyMsg( "tag-$tag", $msg ) ) {
- return htmlspecialchars($tag);
+ return htmlspecialchars( $tag );
}
return $msg;
}
## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id.
- static function addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params = null ) {
- if ( !is_array($tags) ) {
+ static function addTags( $tags, $rc_id = null, $rev_id = null, $log_id = null, $params = null ) {
+ if ( !is_array( $tags ) ) {
$tags = array( $tags );
}
$tags = array_filter( $tags ); // Make sure we're submitting all tags...
- if (!$rc_id && !$rev_id && !$log_id) {
+ if( !$rc_id && !$rev_id && !$log_id ) {
throw new MWException( "At least one of: RCID, revision ID, and log ID MUST be specified when adding a tag to a change!" );
}
$dbr = wfGetDB( DB_SLAVE );
// Might as well look for rcids and so on.
- if (!$rc_id) {
+ if( !$rc_id ) {
$dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave.
- if ($log_id) {
+ if( $log_id ) {
$rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_logid' => $log_id ), __METHOD__ );
- } elseif ($rev_id) {
+ } elseif( $rev_id ) {
$rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_this_oldid' => $rev_id ), __METHOD__ );
}
- } elseif (!$log_id && !$rev_id) {
+ } elseif( !$log_id && !$rev_id ) {
$dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave.
$log_id = $dbr->selectField( 'recentchanges', 'rc_logid', array( 'rc_id' => $rc_id ), __METHOD__ );
$rev_id = $dbr->selectField( 'recentchanges', 'rc_this_oldid', array( 'rc_id' => $rc_id ), __METHOD__ );
@@ -63,8 +70,8 @@ class ChangeTags {
$prevTags = $prevTags ? $prevTags : '';
$prevTags = array_filter( explode( ',', $prevTags ) );
$newTags = array_unique( array_merge( $prevTags, $tags ) );
- sort($prevTags);
- sort($newTags);
+ sort( $prevTags );
+ sort( $newTags );
if ( $prevTags == $newTags ) {
// No change.
@@ -72,15 +79,28 @@ class ChangeTags {
}
$dbw = wfGetDB( DB_MASTER );
- $dbw->replace( 'tag_summary', array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ), __METHOD__ );
+ $dbw->replace(
+ 'tag_summary',
+ array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ),
+ array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ),
+ __METHOD__
+ );
// Insert the tags rows.
$tagsRows = array();
foreach( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally.
- $tagsRows[] = array_filter( array( 'ct_tag' => $tag, 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, 'ct_params' => $params ) );
+ $tagsRows[] = array_filter(
+ array(
+ 'ct_tag' => $tag,
+ 'ct_rc_id' => $rc_id,
+ 'ct_log_id' => $log_id,
+ 'ct_rev_id' => $rev_id,
+ 'ct_params' => $params
+ )
+ );
}
- $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array('IGNORE') );
+ $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array( 'IGNORE' ) );
return true;
}
@@ -89,38 +109,40 @@ class ChangeTags {
* Applies all tags-related changes to a query.
* Handles selecting tags, and filtering.
* Needs $tables to be set up properly, so we can figure out which join conditions to use.
- */
+ */
static function modifyDisplayQuery( &$tables, &$fields, &$conds,
&$join_conds, &$options, $filter_tag = false ) {
global $wgRequest, $wgUseTagFilter;
-
- if ($filter_tag === false) {
+
+ if( $filter_tag === false ) {
$filter_tag = $wgRequest->getVal( 'tagfilter' );
}
// Figure out which conditions can be done.
$join_field = '';
- if ( in_array('recentchanges', $tables) ) {
+ if ( in_array( 'recentchanges', $tables ) ) {
$join_cond = 'rc_id';
- } elseif( in_array('logging', $tables) ) {
+ } elseif( in_array( 'logging', $tables ) ) {
$join_cond = 'log_id';
- } elseif ( in_array('revision', $tables) ) {
+ } elseif ( in_array( 'revision', $tables ) ) {
$join_cond = 'rev_id';
} else {
- throw new MWException( "Unable to determine appropriate JOIN condition for tagging." );
+ throw new MWException( 'Unable to determine appropriate JOIN condition for tagging.' );
}
// JOIN on tag_summary
$tables[] = 'tag_summary';
$join_conds['tag_summary'] = array( 'LEFT JOIN', "ts_$join_cond=$join_cond" );
$fields[] = 'ts_tags';
-
- if ($wgUseTagFilter && $filter_tag) {
+
+ if( $wgUseTagFilter && $filter_tag ) {
// Somebody wants to filter on a tag.
// Add an INNER JOIN on change_tag
// FORCE INDEX -- change_tags will almost ALWAYS be the correct query plan.
- $options['USE INDEX'] = array( 'change_tag' => 'change_tag_tag_id' );
+ global $wgOldChangeTagsIndex;
+ $index = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
+ $options['USE INDEX'] = array( 'change_tag' => $index );
unset( $options['FORCE INDEX'] );
$tables[] = 'change_tag';
$join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" );
@@ -134,15 +156,15 @@ class ChangeTags {
*/
static function buildTagFilterSelector( $selected='', $fullForm = false /* used to put a full form around the selector */ ) {
global $wgUseTagFilter;
-
+
if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) )
return $fullForm ? '' : array();
-
+
global $wgTitle;
-
+
$data = array( wfMsgExt( 'tag-filter', 'parseinline' ), Xml::input( 'tagfilter', 20, $selected ) );
- if (!$fullForm) {
+ if ( !$fullForm ) {
return $data;
}
@@ -160,9 +182,9 @@ class ChangeTags {
global $wgMemc;
$key = wfMemcKey( 'valid-tags' );
- if ($tags = $wgMemc->get( $key ))
+ if ( $tags = $wgMemc->get( $key ) )
return $tags;
-
+
$emptyTags = array();
// Some DB stuff
@@ -171,8 +193,8 @@ class ChangeTags {
while( $row = $res->fetchObject() ) {
$emptyTags[] = $row->vt_tag;
}
-
- wfRunHooks( 'ListDefinedTags', array(&$emptyTags) );
+
+ wfRunHooks( 'ListDefinedTags', array( &$emptyTags ) );
$emptyTags = array_filter( array_unique( $emptyTags ) );
diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php
index a0c2767a..bc50fe02 100644
--- a/includes/ChangesFeed.php
+++ b/includes/ChangesFeed.php
@@ -1,14 +1,31 @@
<?php
+/**
+ * Feed to Special:RecentChanges and Special:RecentChangesLiked
+ *
+ * @ingroup Feed
+ */
class ChangesFeed {
-
public $format, $type, $titleMsg, $descMsg;
+ /**
+ * Constructor
+ *
+ * @param $format String: feed's format (either 'rss' or 'atom')
+ * @param $type String: type of feed (for cache keys)
+ */
public function __construct( $format, $type ) {
$this->format = $format;
$this->type = $type;
}
+ /**
+ * Get a ChannelFeed subclass object to use
+ *
+ * @param $title String: feed's title
+ * @param $description String: feed's description
+ * @return ChannelFeed subclass or false on failure
+ */
public function getFeedObject( $title, $description ) {
global $wgSitename, $wgContLanguageCode, $wgFeedClasses, $wgTitle;
$feedTitle = "$wgSitename - {$title} [$wgContLanguageCode]";
@@ -18,16 +35,26 @@ class ChangesFeed {
$feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() );
}
- public function execute( $feed, $rows, $limit=0, $hideminor=false, $lastmod=false, $target='' ) {
+ /**
+ * Generates feed's content
+ *
+ * @param $feed ChannelFeed subclass object (generally the one returned by getFeedObject())
+ * @param $rows ResultWrapper object with rows in recentchanges table
+ * @param $lastmod Integer: timestamp of the last item in the recentchanges table (only used for the cache key)
+ * @param $opts FormOptions as in SpecialRecentChanges::getDefaultOptions()
+ * @return null or true
+ */
+ public function execute( $feed, $rows, $lastmod, $opts ) {
global $messageMemc, $wgFeedCacheTimeout;
- global $wgSitename, $wgContLanguageCode;
+ global $wgSitename, $wgLang;
if ( !FeedUtils::checkFeedOutput( $this->format ) ) {
return;
}
$timekey = wfMemcKey( $this->type, $this->format, 'timestamp' );
- $key = wfMemcKey( $this->type, $this->format, $limit, $hideminor, $target );
+ $optionsHash = md5( serialize( $opts->getAllValues() ) );
+ $key = wfMemcKey( $this->type, $this->format, $wgLang->getCode(), $optionsHash );
FeedUtils::checkPurge($timekey, $key);
@@ -52,13 +79,28 @@ class ChangesFeed {
return true;
}
+ /**
+ * Save to feed result to $messageMemc
+ *
+ * @param $feed String: feed's content
+ * @param $timekey String: memcached key of the last modification
+ * @param $key String: memcached key of the content
+ */
public function saveToCache( $feed, $timekey, $key ) {
global $messageMemc;
$expire = 3600 * 24; # One day
- $messageMemc->set( $key, $feed );
+ $messageMemc->set( $key, $feed, $expire );
$messageMemc->set( $timekey, wfTimestamp( TS_MW ), $expire );
}
+ /**
+ * Try to load the feed result from $messageMemc
+ *
+ * @param $lastmod Integer: timestamp of the last item in the recentchanges table
+ * @param $timekey String: memcached key of the last modification
+ * @param $key String: memcached key of the content
+ * @return feed's content on cache hit or false on cache miss
+ */
public function loadFromCache( $lastmod, $timekey, $key ) {
global $wgFeedCacheTimeout, $messageMemc;
$feedLastmod = $messageMemc->get( $timekey );
@@ -86,10 +128,10 @@ class ChangesFeed {
}
/**
- * Generate the feed items given a row from the database.
- * @param $rows Database resource with recentchanges rows
- * @param $feed Feed object
- */
+ * Generate the feed items given a row from the database.
+ * @param $rows DatabaseBase resource with recentchanges rows
+ * @param $feed Feed object
+ */
public static function generateFeed( $rows, &$feed ) {
wfProfileIn( __METHOD__ );
@@ -113,18 +155,20 @@ class ChangesFeed {
foreach( $sorted as $obj ) {
$title = Title::makeTitle( $obj->rc_namespace, $obj->rc_title );
$talkpage = $title->getTalkPage();
+ // Skip items with deleted content (avoids partially complete/inconsistent output)
+ if( $obj->rc_deleted ) continue;
$item = new FeedItem(
$title->getPrefixedText(),
FeedUtils::formatDiff( $obj ),
- $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ),
+ $obj->rc_this_oldid ? $title->getFullURL( 'diff=' . $obj->rc_this_oldid . '&oldid=prev' ) : $title->getFullURL(),
$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 4eda1dbd..9f092991 100644
--- a/includes/ChangesList.php
+++ b/includes/ChangesList.php
@@ -25,13 +25,14 @@ class RCCacheEntry extends RecentChange {
class ChangesList {
# Called by history lists and recent changes
public $skin;
+ protected $watchlist = false;
/**
* Changeslist contructor
- * @param Skin $skin
+ * @param $skin Skin
*/
- public function __construct( &$skin ) {
- $this->skin =& $skin;
+ public function __construct( $skin ) {
+ $this->skin = $skin;
$this->preCacheMessages();
}
@@ -44,7 +45,7 @@ class ChangesList {
*/
public static function newFromUser( &$user ) {
$sk = $user->getSkin();
- $list = NULL;
+ $list = null;
if( wfRunHooks( 'FetchChangesList', array( &$user, &$sk, &$list ) ) ) {
return $user->getOption( 'usenewrc' ) ?
new EnhancedChangesList( $sk ) : new OldChangesList( $sk );
@@ -52,6 +53,14 @@ class ChangesList {
return $list;
}
}
+
+ /**
+ * Sets the list to use a <li class="watchlist-(namespace)-(page)"> tag
+ * @param $value Boolean
+ */
+ public function setWatchlistDivs( $value = true ) {
+ $this->watchlist = $value;
+ }
/**
* As we use the same small set of messages in various methods and that
@@ -59,8 +68,8 @@ class ChangesList {
*/
private function preCacheMessages() {
if( !isset( $this->message ) ) {
- foreach( explode(' ', 'cur diff hist minoreditletter newpageletter last '.
- 'blocklink history boteditletter semicolon-separator' ) as $msg ) {
+ foreach ( explode( ' ', 'cur diff hist last blocklink history ' .
+ 'semicolon-separator pipe-separator' ) as $msg ) {
$this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
}
}
@@ -69,26 +78,95 @@ 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
+ * @param $new Boolean
+ * @param $minor Boolean
+ * @param $patrolled Boolean
+ * @param $nothing String to use for empty space
+ * @param $bot Boolean
+ * @return String
*/
protected function recentChangesFlags( $new, $minor, $patrolled, $nothing = '&nbsp;', $bot = false ) {
- $f = $new ?
- '<span class="newpage">' . $this->message['newpageletter'] . '</span>' : $nothing;
- $f .= $minor ?
- '<span class="minor">' . $this->message['minoreditletter'] . '</span>' : $nothing;
- $f .= $bot ? '<span class="bot">' . $this->message['boteditletter'] . '</span>' : $nothing;
- $f .= $patrolled ? '<span class="unpatrolled">!</span>' : $nothing;
+ $f = $new ? self::flag( 'newpage' ) : $nothing;
+ $f .= $minor ? self::flag( 'minor' ) : $nothing;
+ $f .= $bot ? self::flag( 'bot' ) : $nothing;
+ $f .= $patrolled ? self::flag( 'unpatrolled' ) : $nothing;
return $f;
}
/**
+ * Provide the <abbr> element appropriate to a given abbreviated flag,
+ * namely the flag indicating a new page, a minor edit, a bot edit, or an
+ * unpatrolled edit. By default in English it will contain "N", "m", "b",
+ * "!" respectively, plus it will have an appropriate title and class.
+ *
+ * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot'
+ * @return String: Raw HTML
+ */
+ public static function flag( $key ) {
+ static $messages = null;
+ if ( is_null( $messages ) ) {
+ foreach ( explode( ' ', 'minoreditletter boteditletter newpageletter ' .
+ 'unpatrolledletter recentchanges-label-minor recentchanges-label-bot ' .
+ 'recentchanges-label-newpage recentchanges-label-unpatrolled' ) as $msg ) {
+ $messages[$msg] = wfMsgExt( $msg, 'escapenoentities' );
+ }
+ }
+ # Inconsistent naming, bleh
+ if ( $key == 'newpage' || $key == 'unpatrolled' ) {
+ $key2 = $key;
+ } else {
+ $key2 = $key . 'edit';
+ }
+ return "<abbr class=\"$key\" title=\""
+ . $messages["recentchanges-label-$key"] . "\">"
+ . $messages["${key2}letter"]
+ . '</abbr>';
+ }
+
+ /**
+ * Some explanatory wrapper text for the given flag, to be used in a legend
+ * explaining what the flags mean. For instance, "N - new page". See
+ * also flag().
+ *
+ * @param $key String: 'newpage', 'unpatrolled', 'minor', or 'bot'
+ * @return String: Raw HTML
+ */
+ private static function flagLine( $key ) {
+ return wfMsgExt( "recentchanges-legend-$key", array( 'escapenoentities',
+ 'replaceafter' ), self::flag( $key ) );
+ }
+
+ /**
+ * A handy legend to tell users what the little "m", "b", and so on mean.
+ *
+ * @return String: Raw HTML
+ */
+ public static function flagLegend() {
+ global $wgGroupPermissions, $wgLang;
+
+ $flags = array( self::flagLine( 'newpage' ),
+ self::flagLine( 'minor' ) );
+
+ # Don't show info on bot edits unless there's a bot group of some kind
+ foreach ( $wgGroupPermissions as $rights ) {
+ if ( isset( $rights['bot'] ) && $rights['bot'] ) {
+ $flags[] = self::flagLine( 'bot' );
+ break;
+ }
+ }
+
+ if ( self::usePatrol() ) {
+ $flags[] = self::flagLine( 'unpatrolled' );
+ }
+
+ return '<div class="mw-rc-label-legend">' .
+ wfMsgExt( 'recentchanges-label-legend', 'parseinline',
+ $wgLang->commaList( $flags ) ) . '</div>';
+ }
+
+ /**
* Returns text for the start of the tabular part of RC
- * @return string
+ * @return String
*/
public function beginRecentChangesList() {
$this->rc_cache = array();
@@ -101,14 +179,26 @@ class ChangesList {
/**
* Show formatted char difference
- * @param int $old bytes
- * @param int $new bytes
- * @returns string
+ * @param $old Integer: bytes
+ * @param $new Integer: bytes
+ * @returns String
*/
public static function showCharacterDifference( $old, $new ) {
- global $wgRCChangedSizeThreshold, $wgLang;
+ global $wgRCChangedSizeThreshold, $wgLang, $wgMiserMode;
$szdiff = $new - $old;
- $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $wgLang->formatNum( $szdiff ) );
+
+ $code = $wgLang->getCode();
+ static $fastCharDiff = array();
+ if ( !isset($fastCharDiff[$code]) ) {
+ $fastCharDiff[$code] = $wgMiserMode || wfMsgNoTrans( 'rc-change-size' ) === '$1';
+ }
+
+ $formatedSize = $wgLang->formatNum($szdiff);
+
+ if ( !$fastCharDiff[$code] ) {
+ $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $formatedSize );
+ }
+
if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) {
$tag = 'strong';
} else {
@@ -125,7 +215,7 @@ class ChangesList {
/**
* Returns text for the end of RC
- * @return string
+ * @return String
*/
public function endRecentChangesList() {
if( $this->rclistOpen ) {
@@ -135,73 +225,130 @@ class ChangesList {
}
}
- protected function insertMove( &$s, $rc ) {
+ public function insertMove( &$s, $rc ) {
# Diff
$s .= '(' . $this->message['diff'] . ') (';
# Hist
- $s .= $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), $this->message['hist'],
- 'action=history' ) . ') . . ';
+ $s .= $this->skin->link(
+ $rc->getMovedToTitle(),
+ $this->message['hist'],
+ array(),
+ array( 'action' => 'history' ),
+ array( 'known', 'noclasses' )
+ ) . ') . . ';
# "[[x]] moved to [[y]]"
$msg = ( $rc->mAttribs['rc_type'] == RC_MOVE ) ? '1movedto2' : '1movedto2_redir';
- $s .= wfMsg( $msg, $this->skin->makeKnownLinkObj( $rc->getTitle(), '', 'redirect=no' ),
- $this->skin->makeKnownLinkObj( $rc->getMovedToTitle(), '' ) );
+ $s .= wfMsg(
+ $msg,
+ $this->skin->link(
+ $rc->getTitle(),
+ null,
+ array(),
+ array( 'redirect' => 'no' ),
+ array( 'known', 'noclasses' )
+ ),
+ $this->skin->link(
+ $rc->getMovedToTitle(),
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ )
+ );
}
- protected function insertDateHeader( &$s, $rc_timestamp ) {
+ public function insertDateHeader( &$s, $rc_timestamp ) {
global $wgLang;
# Make date header if necessary
$date = $wgLang->date( $rc_timestamp, true, true );
if( $date != $this->lastdate ) {
- if( '' != $this->lastdate ) {
+ if( $this->lastdate != '' ) {
$s .= "</ul>\n";
}
- $s .= '<h4>'.$date."</h4>\n<ul class=\"special\">";
+ $s .= Xml::element( 'h4', null, $date ) . "\n<ul class=\"special\">";
$this->lastdate = $date;
$this->rclistOpen = true;
}
}
- protected function insertLog( &$s, $title, $logtype ) {
+ public function insertLog( &$s, $title, $logtype ) {
$logname = LogPage::logName( $logtype );
- $s .= '(' . $this->skin->makeKnownLinkObj($title, $logname ) . ')';
+ $s .= '(' . $this->skin->link(
+ $title,
+ $logname,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) . ')';
}
- protected function insertDiffHist( &$s, &$rc, $unpatrolled ) {
+ public function insertDiffHist( &$s, &$rc, $unpatrolled ) {
# Diff link
if( $rc->mAttribs['rc_type'] == RC_NEW || $rc->mAttribs['rc_type'] == RC_LOG ) {
$diffLink = $this->message['diff'];
- } else if( !$this->userCan($rc,Revision::DELETED_TEXT) ) {
+ } else if( !self::userCan($rc,Revision::DELETED_TEXT) ) {
$diffLink = $this->message['diff'];
} else {
- $rcidparam = $unpatrolled ? array( 'rcid' => $rc->mAttribs['rc_id'] ) : array();
- $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'],
- wfArrayToCGI( array(
- 'curid' => $rc->mAttribs['rc_cur_id'],
- 'diff' => $rc->mAttribs['rc_this_oldid'],
- 'oldid' => $rc->mAttribs['rc_last_oldid'] ),
- $rcidparam ),
- '', '', ' tabindex="'.$rc->counter.'"');
- }
- $s .= '('.$diffLink.') (';
+ $query = array(
+ 'curid' => $rc->mAttribs['rc_cur_id'],
+ 'diff' => $rc->mAttribs['rc_this_oldid'],
+ 'oldid' => $rc->mAttribs['rc_last_oldid']
+ );
+
+ if( $unpatrolled ) {
+ $query['rcid'] = $rc->mAttribs['rc_id'];
+ };
+
+ $diffLink = $this->skin->link(
+ $rc->getTitle(),
+ $this->message['diff'],
+ array( 'tabindex' => $rc->counter ),
+ $query,
+ array( 'known', 'noclasses' )
+ );
+ }
+ $s .= '(' . $diffLink . $this->message['pipe-separator'];
# History link
- $s .= $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['hist'],
- wfArrayToCGI( array(
+ $s .= $this->skin->link(
+ $rc->getTitle(),
+ $this->message['hist'],
+ array(),
+ array(
'curid' => $rc->mAttribs['rc_cur_id'],
- 'action' => 'history' ) ) );
+ 'action' => 'history'
+ ),
+ array( 'known', 'noclasses' )
+ );
$s .= ') . . ';
}
- protected function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) {
+ public function insertArticleLink( &$s, &$rc, $unpatrolled, $watched ) {
global $wgContLang;
# If it's a new article, there is no diff link, but if it hasn't been
# patrolled yet, we need to give users a way to do so
- $params = ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) ?
- 'rcid='.$rc->mAttribs['rc_id'] : '';
+ $params = array();
+
+ if ( $unpatrolled && $rc->mAttribs['rc_type'] == RC_NEW ) {
+ $params['rcid'] = $rc->mAttribs['rc_id'];
+ }
+
if( $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
- $articlelink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
- $articlelink = '<span class="history-deleted">'.$articlelink.'</span>';
+ $articlelink = $this->skin->link(
+ $rc->getTitle(),
+ null,
+ array(),
+ $params,
+ array( 'known', 'noclasses' )
+ );
+ $articlelink = '<span class="history-deleted">' . $articlelink . '</span>';
} else {
- $articlelink = ' '. $this->skin->makeKnownLinkObj( $rc->getTitle(), '', $params );
+ $articlelink = ' '. $this->skin->link(
+ $rc->getTitle(),
+ null,
+ array(),
+ $params,
+ array( 'known', 'noclasses' )
+ );
}
# Bolden pages watched by this user
if( $watched ) {
@@ -216,7 +363,7 @@ class ChangesList {
$s .= " $articlelink";
}
- protected function insertTimestamp( &$s, $rc ) {
+ public function insertTimestamp( &$s, $rc ) {
global $wgLang;
$s .= $this->message['semicolon-separator'] .
$wgLang->time( $rc->mAttribs['rc_timestamp'], true, true ) . ' . . ';
@@ -233,7 +380,7 @@ class ChangesList {
}
/** insert a formatted action */
- protected function insertAction( &$s, &$rc ) {
+ public function insertAction( &$s, &$rc ) {
if( $rc->mAttribs['rc_type'] == RC_LOG ) {
if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) {
$s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>';
@@ -245,7 +392,7 @@ class ChangesList {
}
/** insert a formatted comment */
- protected function insertComment( &$s, &$rc ) {
+ public function insertComment( &$s, &$rc ) {
if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) {
if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) {
$s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>';
@@ -257,7 +404,7 @@ class ChangesList {
/**
* Check whether to enable recent changes patrol features
- * @return bool
+ * @return Boolean
*/
public static function usePatrol() {
global $wgUser;
@@ -283,9 +430,9 @@ class ChangesList {
/**
* Determine if said field of a revision is hidden
- * @param RCCacheEntry $rc
- * @param int $field one of DELETED_* bitfield constants
- * @return bool
+ * @param $rc RCCacheEntry
+ * @param $field Integer: one of DELETED_* bitfield constants
+ * @return Boolean
*/
public static function isDeleted( $rc, $field ) {
return ( $rc->mAttribs['rc_deleted'] & $field ) == $field;
@@ -294,24 +441,19 @@ class ChangesList {
/**
* 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
+ * @param $rc RCCacheEntry
+ * @param $field Integer
+ * @return Boolean
*/
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 );
+ if( $rc->mAttribs['rc_type'] == RC_LOG ) {
+ return LogEventsList::userCanBitfield( $rc->mAttribs['rc_deleted'], $field );
} else {
- return true;
+ return Revision::userCanBitfield( $rc->mAttribs['rc_deleted'], $field );
}
}
- protected function maybeWatchedLink( $link, $watched=false ) {
+ protected function maybeWatchedLink( $link, $watched = false ) {
if( $watched ) {
return '<strong class="mw-watched">' . $link . '</strong>';
} else {
@@ -320,7 +462,7 @@ class ChangesList {
}
/** Inserts a rollback link */
- protected function insertRollback( &$s, &$rc ) {
+ public function insertRollback( &$s, &$rc ) {
global $wgUser;
if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) {
$page = $rc->getTitle();
@@ -340,7 +482,7 @@ class ChangesList {
}
}
- protected function insertTags( &$s, &$rc, &$classes ) {
+ public function insertTags( &$s, &$rc, &$classes ) {
if ( empty($rc->mAttribs['ts_tags']) )
return;
@@ -349,7 +491,7 @@ class ChangesList {
$s .= ' ' . $tagSummary;
}
- protected function insertExtra( &$s, &$rc, &$classes ) {
+ public function insertExtra( &$s, &$rc, &$classes ) {
## Empty, used for subclassers to add anything special.
}
}
@@ -362,8 +504,8 @@ class OldChangesList extends ChangesList {
/**
* Format a line using the old system (aka without any javascript).
*/
- public function recentChangesLine( &$rc, $watched = false, $linenumber = NULL ) {
- global $wgContLang, $wgLang, $wgRCShowChangedSize, $wgUser;
+ public function recentChangesLine( &$rc, $watched = false, $linenumber = null ) {
+ global $wgLang, $wgRCShowChangedSize, $wgUser;
wfProfileIn( __METHOD__ );
# Should patrol-related stuff be shown?
$unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled'];
@@ -426,20 +568,20 @@ class OldChangesList extends ChangesList {
# For subclasses
$this->insertExtra( $s, $rc, $classes );
- # Mark revision as deleted if so
- if( !$rc->mAttribs['rc_log_type'] && $this->isDeleted($rc,Revision::DELETED_TEXT) ) {
- $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
- }
# How many users watch this page
if( $rc->numberofWatchingusers > 0 ) {
$s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview',
array( 'parsemag', 'escape' ), $wgLang->formatNum( $rc->numberofWatchingusers ) );
}
-
+
+ if( $this->watchlist ) {
+ $classes[] = Sanitizer::escapeClass( 'watchlist-'.$rc->mAttribs['rc_namespace'].'-'.$rc->mAttribs['rc_title'] );
+ }
+
wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) );
wfProfileOut( __METHOD__ );
- return "$dateheader<li class=\"".implode( ' ', $classes )."\">$s</li>\n";
+ return "$dateheader<li class=\"".implode( ' ', $classes )."\">".$s."</li>\n";
}
}
@@ -449,26 +591,24 @@ class OldChangesList extends ChangesList {
*/
class EnhancedChangesList extends ChangesList {
/**
- * Add the JavaScript file for enhanced changeslist
- * @ return string
- */
+ * Add the JavaScript file for enhanced changeslist
+ * @return String
+ */
public function beginRecentChangesList() {
- global $wgStylePath, $wgJsMimeType, $wgStyleVersion;
+ global $wgStylePath, $wgStyleVersion;
$this->rc_cache = array();
$this->rcMoveIndex = 0;
$this->rcCacheIndex = 0;
$this->lastdate = '';
$this->rclistOpen = false;
- $script = Xml::tags( 'script', array(
- 'type' => $wgJsMimeType,
- 'src' => $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" ), '' );
+ $script = Html::linkedScript( $wgStylePath . "/common/enhancedchanges.js?$wgStyleVersion" );
return $script;
}
/**
* Format a line for enhanced recentchange (aka with javascript and block of lines).
*/
public function recentChangesLine( &$baseRC, $watched = false ) {
- global $wgLang, $wgContLang, $wgUser;
+ global $wgLang, $wgUser;
wfProfileIn( __METHOD__ );
@@ -479,7 +619,7 @@ class EnhancedChangesList extends ChangesList {
// FIXME: Would be good to replace this extract() call with something
// that explicitly initializes variables.
extract( $rc->mAttribs );
- $curIdEq = 'curid=' . $rc_cur_id;
+ $curIdEq = array( 'curid' => $rc_cur_id );
# If it's a new day, add the headline and flush the cache
$date = $wgLang->date( $rc_timestamp, true );
@@ -488,7 +628,7 @@ class EnhancedChangesList extends ChangesList {
# Process current cache
$ret = $this->recentChangesBlock();
$this->rc_cache = array();
- $ret .= "<h4>{$date}</h4>\n";
+ $ret .= Xml::element( 'h4', null, $date );
$this->lastdate = $date;
}
@@ -504,19 +644,21 @@ class EnhancedChangesList extends ChangesList {
// 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(), '' ) );
+ $clink = wfMsg( $msg, $this->skin->linkKnown( $rc->getTitle(), null,
+ array(), array( 'redirect' => 'no' ) ),
+ $this->skin->linkKnown( $rc->getMovedToTitle() ) );
// New unpatrolled pages
} else if( $rc->unpatrolled && $rc_type == RC_NEW ) {
- $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '', "rcid={$rc_id}" );
+ $clink = $this->skin->linkKnown( $rc->getTitle(), null, array(),
+ array( '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,
+ $clink = '(' . $this->skin->linkKnown( $logtitle,
LogPage::logName($rc_log_type) ) . ')';
} else {
- $clink = $this->skin->makeLinkObj( $rc->getTitle(), '' );
+ $clink = $this->skin->link( $rc->getTitle() );
}
$watched = false;
// Log entries (old format) and special pages
@@ -525,14 +667,14 @@ class EnhancedChangesList extends ChangesList {
if ( $specialName == 'Log' ) {
# Log updates, etc
$logname = LogPage::logName( $logtype );
- $clink = '(' . $this->skin->makeKnownLinkObj( $rc->getTitle(), $logname ) . ')';
+ $clink = '(' . $this->skin->linkKnown( $rc->getTitle(), $logname ) . ')';
} else {
wfDebug( "Unexpected special page in recentchanges\n" );
$clink = '';
}
// Edits
} else {
- $clink = $this->skin->makeKnownLinkObj( $rc->getTitle(), '' );
+ $clink = $this->skin->linkKnown( $rc->getTitle() );
}
# Don't show unusable diff links
@@ -540,44 +682,49 @@ class EnhancedChangesList extends ChangesList {
$showdifflinks = false;
}
- $time = $wgContLang->time( $rc_timestamp, true, true );
+ $time = $wgLang->time( $rc_timestamp, true, true );
$rc->watched = $watched;
$rc->link = $clink;
$rc->timestamp = $time;
$rc->numberofWatchingusers = $baseRC->numberofWatchingusers;
- # Make "cur" and "diff" links
+ # Make "cur" and "diff" links. Do not use link(), it is too slow if
+ # called too many times (50% of CPU time on RecentChanges!).
if( $rc->unpatrolled ) {
- $rcIdQuery = "&rcid={$rc_id}";
+ $rcIdQuery = array( 'rcid' => $rc_id );
} else {
- $rcIdQuery = '';
+ $rcIdQuery = array();
}
- $querycur = $curIdEq."&diff=0&oldid=$rc_this_oldid";
- $querydiff = $curIdEq."&diff=$rc_this_oldid&oldid=$rc_last_oldid$rcIdQuery";
- $aprops = ' tabindex="'.$baseRC->counter.'"';
- $curLink = $this->skin->makeKnownLinkObj( $rc->getTitle(),
- $this->message['cur'], $querycur, '' ,'', $aprops );
+ $querycur = $curIdEq + array( 'diff' => '0', 'oldid' => $rc_this_oldid );
+ $querydiff = $curIdEq + array( 'diff' => $rc_this_oldid, 'oldid' =>
+ $rc_last_oldid ) + $rcIdQuery;
- # Make "diff" an "cur" links
if( !$showdifflinks ) {
- $curLink = $this->message['cur'];
- $diffLink = $this->message['diff'];
+ $curLink = $this->message['cur'];
+ $diffLink = $this->message['diff'];
} else if( in_array( $rc_type, array(RC_NEW,RC_LOG,RC_MOVE,RC_MOVE_OVER_REDIRECT) ) ) {
- $curLink = ($rc_type != RC_NEW) ? $this->message['cur'] : $curLink;
+ if ( $rc_type != RC_NEW ) {
+ $curLink = $this->message['cur'];
+ } else {
+ $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) );
+ $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>";
+ }
$diffLink = $this->message['diff'];
} else {
- $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'],
- $querydiff, '' ,'', $aprops );
+ $diffUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querydiff ) );
+ $curUrl = htmlspecialchars( $rc->getTitle()->getLinkUrl( $querycur ) );
+ $diffLink = "<a href=\"$diffUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['diff']}</a>";
+ $curLink = "<a href=\"$curUrl\" tabindex=\"{$baseRC->counter}\">{$this->message['cur']}</a>";
}
# Make "last" link
if( !$showdifflinks || !$rc_last_oldid ) {
- $lastLink = $this->message['last'];
+ $lastLink = $this->message['last'];
} else if( $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 );
+ $lastLink = $this->skin->linkKnown( $rc->getTitle(), $this->message['last'],
+ array(), $curIdEq + array('diff' => $rc_this_oldid, 'oldid' => $rc_last_oldid) + $rcIdQuery );
}
# Make user links
@@ -624,7 +771,7 @@ class EnhancedChangesList extends ChangesList {
wfProfileIn( __METHOD__ );
- $r = '<table cellpadding="0" cellspacing="0" border="0" style="background: none"><tr>';
+ $r = '<table class="mw-enhanced-rc"><tr>';
# Collate list of users
$userlinks = array();
@@ -694,13 +841,13 @@ class EnhancedChangesList extends ChangesList {
$tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>";
$tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>";
- $r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.'&nbsp;';
+ $r .= '<td class="mw-enhanced-rc">'.$tl.'&nbsp;';
# Main line
$r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, '&nbsp;', $bot );
# Timestamp
- $r .= '&nbsp;'.$block[0]->timestamp.'&nbsp;</tt></td><td>';
+ $r .= '&nbsp;'.$block[0]->timestamp.'&nbsp;</td><td style="padding:0px;">';
# Article link
if( $namehidden ) {
@@ -713,7 +860,7 @@ class EnhancedChangesList extends ChangesList {
$r .= $wgContLang->getDirMark();
- $curIdEq = 'curid=' . $curId;
+ $queryParams['curid'] = $curId;
# Changes message
$n = count($block);
static $nchanges = array();
@@ -729,8 +876,17 @@ class EnhancedChangesList extends ChangesList {
} else if( $isnew ) {
$r .= $nchanges[$n];
} else {
- $r .= $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
- $nchanges[$n], $curIdEq."&diff=$currentRevision&oldid=$oldid" );
+ $params = $queryParams;
+ $params['diff'] = $currentRevision;
+ $params['oldid'] = $oldid;
+
+ $r .= $this->skin->link(
+ $block[0]->getTitle(),
+ $nchanges[$n],
+ array(),
+ $params,
+ array( 'known', 'noclasses' )
+ );
}
}
@@ -738,10 +894,19 @@ class EnhancedChangesList extends ChangesList {
if( $allLogs ) {
// don't show history link for logs
} else if( $namehidden || !$block[0]->getTitle()->exists() ) {
- $r .= $this->message['semicolon-separator'] . $this->message['hist'] . ')';
+ $r .= $this->message['pipe-separator'] . $this->message['hist'] . ')';
} else {
- $r .= $this->message['semicolon-separator'] . $this->skin->makeKnownLinkObj( $block[0]->getTitle(),
- $this->message['hist'], $curIdEq . '&action=history' ) . ')';
+ $params = $queryParams;
+ $params['action'] = 'history';
+
+ $r .= $this->message['pipe-separator'] .
+ $this->skin->link(
+ $block[0]->getTitle(),
+ $this->message['hist'],
+ array(),
+ $params,
+ array( 'known', 'noclasses' )
+ ) . ')';
}
$r .= ' . . ';
@@ -750,10 +915,10 @@ class EnhancedChangesList extends ChangesList {
$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 ) {
+ while( $last < $first && $block[$last]->mAttribs['rc_new_len'] === null ) {
$last++;
}
- while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === NULL ) {
+ while( $first > $last && $block[$first]->mAttribs['rc_old_len'] === null ) {
$first--;
}
# Get net change
@@ -774,7 +939,7 @@ class EnhancedChangesList extends ChangesList {
# Sub-entries
$r .= '<div id="mw-rc-subentries-'.$jsid.'" class="mw-changeslist-hidden">';
- $r .= '<table cellpadding="0" cellspacing="0" border="0" style="background: none">';
+ $r .= '<table class="mw-enhanced-rc">';
foreach( $block as $rcObj ) {
# Extract fields from DB into the function scope (rc_xxxx variables)
// FIXME: Would be good to replace this extract() call with something
@@ -784,35 +949,44 @@ class EnhancedChangesList extends ChangesList {
extract( $rcObj->mAttribs );
#$r .= '<tr><td valign="top">'.$this->spacerArrow();
- $r .= '<tr><td valign="top">';
- $r .= '<tt>'.$this->spacerIndent() . $this->spacerIndent();
+ $r .= '<tr><td style="vertical-align:top;font-family:monospace; padding:0px;">';
+ $r .= $this->spacerIndent() . $this->spacerIndent();
$r .= $this->recentChangesFlags( $rc_new, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
- $r .= '&nbsp;</tt></td><td valign="top">';
+ $r .= '&nbsp;</td><td style="vertical-align:top; padding:0px;"><span style="font-family:monospace">';
+
+ $params = $queryParams;
- $o = '';
if( $rc_this_oldid != 0 ) {
- $o = 'oldid='.$rc_this_oldid;
+ $params['oldid'] = $rc_this_oldid;
}
+
# Log timestamp
if( $rc_type == RC_LOG ) {
- $link = '<tt>'.$rcObj->timestamp.'</tt> ';
+ $link = $rcObj->timestamp;
# Revision link
} else if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) {
- $link = '<span class="history-deleted"><tt>'.$rcObj->timestamp.'</tt></span> ';
+ $link = '<span class="history-deleted">'.$rcObj->timestamp.'</span> ';
} else {
- $rcIdEq = ($rcObj->unpatrolled && $rc_type == RC_NEW) ?
- '&rcid='.$rcObj->mAttribs['rc_id'] : '';
- $link = '<tt>'.$this->skin->makeKnownLinkObj( $rcObj->getTitle(),
- $rcObj->timestamp, $curIdEq.'&'.$o.$rcIdEq ).'</tt>';
+ if ( $rcObj->unpatrolled && $rc_type == RC_NEW) {
+ $params['rcid'] = $rcObj->mAttribs['rc_id'];
+ }
+
+ $link = $this->skin->link(
+ $rcObj->getTitle(),
+ $rcObj->timestamp,
+ array(),
+ $params,
+ array( 'known', 'noclasses' )
+ );
if( $this->isDeleted($rcObj,Revision::DELETED_TEXT) )
$link = '<span class="history-deleted">'.$link.'</span> ';
}
- $r .= $link;
+ $r .= $link . '</span>';
if ( !$rc_type == RC_LOG || $rc_type == RC_NEW ) {
$r .= ' (';
$r .= $rcObj->curlink;
- $r .= $this->message['semicolon-separator'];
+ $r .= $this->message['pipe-separator'];
$r .= $rcObj->lastlink;
$r .= ')';
}
@@ -833,11 +1007,6 @@ class EnhancedChangesList extends ChangesList {
$this->insertRollback( $r, $rcObj );
# Tags
$this->insertTags( $r, $rcObj, $classes );
-
- # Mark revision as deleted
- if( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) {
- $r .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
- }
$r .= "</td></tr>\n";
}
@@ -852,10 +1021,10 @@ class EnhancedChangesList extends ChangesList {
/**
* Generate HTML for an arrow or placeholder graphic
- * @param string $dir one of '', 'd', 'l', 'r'
- * @param string $alt text
- * @param string $title text
- * @return string HTML <img> tag
+ * @param $dir String: one of '', 'd', 'l', 'r'
+ * @param $alt String: text
+ * @param $title String: text
+ * @return String: HTML <img> tag
*/
protected function arrow( $dir, $alt='', $title='' ) {
global $wgStylePath;
@@ -868,7 +1037,7 @@ class EnhancedChangesList extends ChangesList {
/**
* Generate HTML for a right- or left-facing arrow,
* depending on language direction.
- * @return string HTML <img> tag
+ * @return String: HTML <img> tag
*/
protected function sideArrow() {
global $wgContLang;
@@ -879,7 +1048,7 @@ class EnhancedChangesList extends ChangesList {
/**
* Generate HTML for a down-facing arrow
* depending on language direction.
- * @return string HTML <img> tag
+ * @return String: HTML <img> tag
*/
protected function downArrow() {
return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) );
@@ -887,7 +1056,7 @@ class EnhancedChangesList extends ChangesList {
/**
* Generate HTML for a spacer image
- * @return string HTML <img> tag
+ * @return String: HTML <img> tag
*/
protected function spacerArrow() {
return $this->arrow( '', codepointToUtf8( 0xa0 ) ); // non-breaking space
@@ -895,7 +1064,7 @@ class EnhancedChangesList extends ChangesList {
/**
* Add a set of spaces
- * @return string HTML <td> tag
+ * @return String: HTML <td> tag
*/
protected function spacerIndent() {
return '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
@@ -903,10 +1072,10 @@ class EnhancedChangesList extends ChangesList {
/**
* Enhanced RC ungrouped line.
- * @return string a HTML formated line (generated using $r)
+ * @return String: a HTML formated line (generated using $r)
*/
protected function recentChangesBlockLine( $rcObj ) {
- global $wgContLang, $wgRCShowChangedSize;
+ global $wgRCShowChangedSize;
wfProfileIn( __METHOD__ );
@@ -915,30 +1084,42 @@ class EnhancedChangesList extends ChangesList {
// that explicitly initializes variables.
$classes = array(); // TODO implement
extract( $rcObj->mAttribs );
- $curIdEq = "curid={$rc_cur_id}";
+ $query['curid'] = $rc_cur_id;
- $r = '<table cellspacing="0" cellpadding="0" border="0" style="background: none"><tr>';
- $r .= '<td valign="top" style="white-space: nowrap"><tt>' . $this->spacerArrow() . '&nbsp;';
+ $r = '<table class="mw-enhanced-rc"><tr>';
+ $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow() . '&nbsp;';
# Flag and Timestamp
if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) {
$r .= '&nbsp;&nbsp;&nbsp;&nbsp;'; // 4 flags -> 4 spaces
} else {
$r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, '&nbsp;', $rc_bot );
}
- $r .= '&nbsp;'.$rcObj->timestamp.'&nbsp;</tt></td><td>';
+ $r .= '&nbsp;'.$rcObj->timestamp.'&nbsp;</td><td style="padding:0px;">';
# 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 ) . ')';
+ $r .= '(' . $this->skin->link(
+ $logtitle,
+ $logname,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) . ')';
} else {
$this->insertArticleLink( $r, $rcObj, $rcObj->unpatrolled, $rcObj->watched );
}
# Diff and hist links
if ( $rc_type != RC_LOG ) {
- $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator'];
- $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $this->message['hist'],
- $curIdEq.'&action=history' ) . ')';
+ $r .= ' ('. $rcObj->difflink . $this->message['pipe-separator'];
+ $query['action'] = 'history';
+ $r .= $this->skin->link(
+ $rcObj->getTitle(),
+ $this->message['hist'],
+ array(),
+ $query,
+ array( 'known', 'noclasses' )
+ ) . ')';
}
$r .= ' . . ';
# Character diff
diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php
new file mode 100644
index 00000000..f862ebb7
--- /dev/null
+++ b/includes/ConfEditor.php
@@ -0,0 +1,1058 @@
+<?php
+
+/**
+ * This is a state machine style parser with two internal stacks:
+ * * A next state stack, which determines the state the machine will progress to next
+ * * A path stack, which keeps track of the logical location in the file.
+ *
+ * Reference grammar:
+ *
+ * file = T_OPEN_TAG *statement
+ * statement = T_VARIABLE "=" expression ";"
+ * expression = array / scalar / T_VARIABLE
+ * array = T_ARRAY "(" [ element *( "," element ) [ "," ] ] ")"
+ * element = assoc-element / expression
+ * assoc-element = scalar T_DOUBLE_ARROW expression
+ * scalar = T_LNUMBER / T_DNUMBER / T_STRING / T_CONSTANT_ENCAPSED_STRING
+ */
+class ConfEditor {
+ /** The text to parse */
+ var $text;
+
+ /** The token array from token_get_all() */
+ var $tokens;
+
+ /** The current position in the token array */
+ var $pos;
+
+ /** The current 1-based line number */
+ var $lineNum;
+
+ /** The current 1-based column number */
+ var $colNum;
+
+ /** The current 0-based byte number */
+ var $byteNum;
+
+ /** The current ConfEditorToken object */
+ var $currentToken;
+
+ /** The previous ConfEditorToken object */
+ var $prevToken;
+
+ /**
+ * The state machine stack. This is an array of strings where the topmost
+ * element will be popped off and become the next parser state.
+ */
+ var $stateStack;
+
+
+ /**
+ * The path stack is a stack of associative arrays with the following elements:
+ * name The name of top level of the path
+ * level The level (number of elements) of the path
+ * startByte The byte offset of the start of the path
+ * startToken The token offset of the start
+ * endByte The byte offset of thee
+ * endToken The token offset of the end, plus one
+ * valueStartToken The start token offset of the value part
+ * valueStartByte The start byte offset of the value part
+ * valueEndToken The end token offset of the value part, plus one
+ * valueEndByte The end byte offset of the value part, plus one
+ * nextArrayIndex The next numeric array index at this level
+ * hasComma True if the array element ends with a comma
+ * arrowByte The byte offset of the "=>", or false if there isn't one
+ */
+ var $pathStack;
+
+ /**
+ * The elements of the top of the pathStack for every path encountered, indexed
+ * by slash-separated path.
+ */
+ var $pathInfo;
+
+ /**
+ * Next serial number for whitespace placeholder paths (@extra-N)
+ */
+ var $serial;
+
+ /**
+ * Editor state. This consists of the internal copy/insert operations which
+ * are applied to the source string to obtain the destination string.
+ */
+ var $edits;
+
+ /**
+ * Simple entry point for command-line testing
+ */
+ static function test( $text ) {
+ try {
+ $ce = new self( $text );
+ $ce->parse();
+ } catch ( ConfEditorParseError $e ) {
+ return $e->getMessage() . "\n" . $e->highlight( $text );
+ }
+ return "OK";
+ }
+
+ /**
+ * Construct a new parser
+ */
+ public function __construct( $text ) {
+ $this->text = $text;
+ }
+
+ /**
+ * Edit the text. Returns the edited text.
+ * @param array $ops Array of operations.
+ *
+ * Operations are given as an associative array, with members:
+ * type: One of delete, set, append or insert (required)
+ * path: The path to operate on (required)
+ * key: The array key to insert/append, with PHP quotes
+ * value: The value, with PHP quotes
+ *
+ * delete
+ * Deletes an array element or statement with the specified path.
+ * e.g.
+ * array('type' => 'delete', 'path' => '$foo/bar/baz' )
+ * is equivalent to the runtime PHP code:
+ * unset( $foo['bar']['baz'] );
+ *
+ * set
+ * Sets the value of an array element. If the element doesn't exist, it
+ * is appended to the array. If it does exist, the value is set, with
+ * comments and indenting preserved.
+ *
+ * append
+ * Appends a new element to the end of the array. Adds a trailing comma.
+ * e.g.
+ * array( 'type' => 'append', 'path', '$foo/bar',
+ * 'key' => 'baz', 'value' => "'x'" )
+ * is like the PHP code:
+ * $foo['bar']['baz'] = 'x';
+ *
+ * insert
+ * Insert a new element at the start of the array.
+ *
+ */
+ public function edit( $ops ) {
+ $this->parse();
+
+ $this->edits = array(
+ array( 'copy', 0, strlen( $this->text ) )
+ );
+ foreach ( $ops as $op ) {
+ $type = $op['type'];
+ $path = $op['path'];
+ $value = isset( $op['value'] ) ? $op['value'] : null;
+ $key = isset( $op['key'] ) ? $op['key'] : null;
+
+ switch ( $type ) {
+ case 'delete':
+ list( $start, $end ) = $this->findDeletionRegion( $path );
+ $this->replaceSourceRegion( $start, $end, false );
+ break;
+ case 'set':
+ if ( isset( $this->pathInfo[$path] ) ) {
+ list( $start, $end ) = $this->findValueRegion( $path );
+ $encValue = $value; // var_export( $value, true );
+ $this->replaceSourceRegion( $start, $end, $encValue );
+ break;
+ }
+ // No existing path, fall through to append
+ $slashPos = strrpos( $path, '/' );
+ $key = var_export( substr( $path, $slashPos + 1 ), true );
+ $path = substr( $path, 0, $slashPos );
+ // Fall through
+ case 'append':
+ // Find the last array element
+ $lastEltPath = $this->findLastArrayElement( $path );
+ if ( $lastEltPath === false ) {
+ throw new MWException( "Can't find any element of array \"$path\"" );
+ }
+ $lastEltInfo = $this->pathInfo[$lastEltPath];
+
+ // Has it got a comma already?
+ if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) {
+ // No comma, insert one after the value region
+ list( $start, $end ) = $this->findValueRegion( $lastEltPath );
+ $this->replaceSourceRegion( $end - 1, $end - 1, ',' );
+ }
+
+ // Make the text to insert
+ list( $start, $end ) = $this->findDeletionRegion( $lastEltPath );
+
+ if ( $key === null ) {
+ list( $indent, $arrowIndent ) = $this->getIndent( $start );
+ $textToInsert = "$indent$value,";
+ } else {
+ list( $indent, $arrowIndent ) =
+ $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] );
+ $textToInsert = "$indent$key$arrowIndent=> $value,";
+ }
+ $textToInsert .= ( $indent === false ? ' ' : "\n" );
+
+ // Insert the item
+ $this->replaceSourceRegion( $end, $end, $textToInsert );
+ break;
+ case 'insert':
+ // Find first array element
+ $firstEltPath = $this->findFirstArrayElement( $path );
+ if ( $firstEltPath === false ) {
+ throw new MWException( "Can't find array element of \"$path\"" );
+ }
+ list( $start, $end ) = $this->findDeletionRegion( $firstEltPath );
+ $info = $this->pathInfo[$firstEltPath];
+
+ // Make the text to insert
+ if ( $key === null ) {
+ list( $indent, $arrowIndent ) = $this->getIndent( $start );
+ $textToInsert = "$indent$value,";
+ } else {
+ list( $indent, $arrowIndent ) =
+ $this->getIndent( $start, $key, $info['arrowByte'] );
+ $textToInsert = "$indent$key$arrowIndent=> $value,";
+ }
+ $textToInsert .= ( $indent === false ? ' ' : "\n" );
+
+ // Insert the item
+ $this->replaceSourceRegion( $start, $start, $textToInsert );
+ break;
+ default:
+ throw new MWException( "Unrecognised operation: \"$type\"" );
+ }
+ }
+
+ // Do the edits
+ $out = '';
+ foreach ( $this->edits as $edit ) {
+ if ( $edit[0] == 'copy' ) {
+ $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] );
+ } else { // if ( $edit[0] == 'insert' )
+ $out .= $edit[1];
+ }
+ }
+
+ // Do a second parse as a sanity check
+ $this->text = $out;
+ try {
+ $this->parse();
+ } catch ( ConfEditorParseError $e ) {
+ throw new MWException(
+ "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " .
+ $e->getMessage() );
+ }
+ return $out;
+ }
+
+ /**
+ * Get the variables defined in the text
+ * @return array( varname => value )
+ */
+ function getVars() {
+ $vars = array();
+ $this->parse();
+ foreach( $this->pathInfo as $path => $data ) {
+ if ( $path[0] != '$' )
+ continue;
+ $trimmedPath = substr( $path, 1 );
+ $name = $data['name'];
+ if ( $name[0] == '@' )
+ continue;
+ if ( $name[0] == '$' )
+ $name = substr( $name, 1 );
+ $parentPath = substr( $trimmedPath, 0,
+ strlen( $trimmedPath ) - strlen( $name ) );
+ if( substr( $parentPath, -1 ) == '/' )
+ $parentPath = substr( $parentPath, 0, -1 );
+
+ $value = substr( $this->text, $data['valueStartByte'],
+ $data['valueEndByte'] - $data['valueStartByte']
+ );
+ $this->setVar( $vars, $parentPath, $name,
+ $this->parseScalar( $value ) );
+ }
+ return $vars;
+ }
+
+ /**
+ * Set a value in an array, unless it's set already. For instance,
+ * setVar( $arr, 'foo/bar', 'baz', 3 ); will set
+ * $arr['foo']['bar']['baz'] = 3;
+ * @param $array array
+ * @param $path string slash-delimited path
+ * @param $key mixed Key
+ * @param $value mixed Value
+ */
+ function setVar( &$array, $path, $key, $value ) {
+ $pathArr = explode( '/', $path );
+ $target =& $array;
+ if ( $path !== '' ) {
+ foreach ( $pathArr as $p ) {
+ if( !isset( $target[$p] ) )
+ $target[$p] = array();
+ $target =& $target[$p];
+ }
+ }
+ if ( !isset( $target[$key] ) )
+ $target[$key] = $value;
+ }
+
+ /**
+ * Parse a scalar value in PHP
+ * @return mixed Parsed value
+ */
+ function parseScalar( $str ) {
+ if ( $str !== '' && $str[0] == '\'' )
+ // Single-quoted string
+ // @todo Fixme: trim() call is due to mystery bug where whitespace gets
+ // appended to the token; without it we ended up reading in the
+ // extra quote on the end!
+ return strtr( substr( trim( $str ), 1, -1 ),
+ array( '\\\'' => '\'', '\\\\' => '\\' ) );
+ if ( $str !== '' && @$str[0] == '"' )
+ // Double-quoted string
+ // @todo Fixme: trim() call is due to mystery bug where whitespace gets
+ // appended to the token; without it we ended up reading in the
+ // extra quote on the end!
+ return stripcslashes( substr( trim( $str ), 1, -1 ) );
+ if ( substr( $str, 0, 4 ) == 'true' )
+ return true;
+ if ( substr( $str, 0, 5 ) == 'false' )
+ return false;
+ if ( substr( $str, 0, 4 ) == 'null' )
+ return null;
+ // Must be some kind of numeric value, so let PHP's weak typing
+ // be useful for a change
+ return $str;
+ }
+
+ /**
+ * Replace the byte offset region of the source with $newText.
+ * Works by adding elements to the $this->edits array.
+ */
+ function replaceSourceRegion( $start, $end, $newText = false ) {
+ // Split all copy operations with a source corresponding to the region
+ // in question.
+ $newEdits = array();
+ foreach ( $this->edits as $i => $edit ) {
+ if ( $edit[0] !== 'copy' ) {
+ $newEdits[] = $edit;
+ continue;
+ }
+ $copyStart = $edit[1];
+ $copyEnd = $edit[2];
+ if ( $start >= $copyEnd || $end <= $copyStart ) {
+ // Outside this region
+ $newEdits[] = $edit;
+ continue;
+ }
+ if ( ( $start < $copyStart && $end > $copyStart )
+ || ( $start < $copyEnd && $end > $copyEnd )
+ ) {
+ throw new MWException( "Overlapping regions found, can't do the edit" );
+ }
+ // Split the copy
+ $newEdits[] = array( 'copy', $copyStart, $start );
+ if ( $newText !== false ) {
+ $newEdits[] = array( 'insert', $newText );
+ }
+ $newEdits[] = array( 'copy', $end, $copyEnd );
+ }
+ $this->edits = $newEdits;
+ }
+
+ /**
+ * Finds the source byte region which you would want to delete, if $pathName
+ * was to be deleted. Includes the leading spaces and tabs, the trailing line
+ * break, and any comments in between.
+ */
+ function findDeletionRegion( $pathName ) {
+ if ( !isset( $this->pathInfo[$pathName] ) ) {
+ throw new MWException( "Can't find path \"$pathName\"" );
+ }
+ $path = $this->pathInfo[$pathName];
+ // Find the start
+ $this->firstToken();
+ while ( $this->pos != $path['startToken'] ) {
+ $this->nextToken();
+ }
+ $regionStart = $path['startByte'];
+ for ( $offset = -1; $offset >= -$this->pos; $offset-- ) {
+ $token = $this->getTokenAhead( $offset );
+ if ( !$token->isSkip() ) {
+ // If there is other content on the same line, don't move the start point
+ // back, because that will cause the regions to overlap.
+ $regionStart = $path['startByte'];
+ break;
+ }
+ $lfPos = strrpos( $token->text, "\n" );
+ if ( $lfPos === false ) {
+ $regionStart -= strlen( $token->text );
+ } else {
+ // The line start does not include the LF
+ $regionStart -= strlen( $token->text ) - $lfPos - 1;
+ break;
+ }
+ }
+ // Find the end
+ while ( $this->pos != $path['endToken'] ) {
+ $this->nextToken();
+ }
+ $regionEnd = $path['endByte']; // past the end
+ for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) {
+ $token = $this->getTokenAhead( $offset );
+ if ( !$token->isSkip() ) {
+ break;
+ }
+ $lfPos = strpos( $token->text, "\n" );
+ if ( $lfPos === false ) {
+ $regionEnd += strlen( $token->text );
+ } else {
+ // This should point past the LF
+ $regionEnd += $lfPos + 1;
+ break;
+ }
+ }
+ return array( $regionStart, $regionEnd );
+ }
+
+ /**
+ * Find the byte region in the source corresponding to the value part.
+ * This includes the quotes, but does not include the trailing comma
+ * or semicolon.
+ *
+ * The end position is the past-the-end (end + 1) value as per convention.
+ */
+ function findValueRegion( $pathName ) {
+ if ( !isset( $this->pathInfo[$pathName] ) ) {
+ throw new MWEXception( "Can't find path \"$pathName\"" );
+ }
+ $path = $this->pathInfo[$pathName];
+ if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) {
+ throw new MWException( "Can't find value region for path \"$pathName\"" );
+ }
+ return array( $path['valueStartByte'], $path['valueEndByte'] );
+ }
+
+ /**
+ * Find the path name of the last element in the array.
+ * If the array is empty, this will return the @extra interstitial element.
+ * If the specified path is not found or is not an array, it will return false.
+ */
+ function findLastArrayElement( $path ) {
+ // Try for a real element
+ $lastEltPath = false;
+ foreach ( $this->pathInfo as $candidatePath => $info ) {
+ $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
+ $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
+ if ( $part2 == '@' ) {
+ // Do nothing
+ } elseif ( $part1 == "$path/" ) {
+ $lastEltPath = $candidatePath;
+ } elseif ( $lastEltPath !== false ) {
+ break;
+ }
+ }
+ if ( $lastEltPath !== false ) {
+ return $lastEltPath;
+ }
+
+ // Try for an interstitial element
+ $extraPath = false;
+ foreach ( $this->pathInfo as $candidatePath => $info ) {
+ $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
+ if ( $part1 == "$path/" ) {
+ $extraPath = $candidatePath;
+ } elseif ( $extraPath !== false ) {
+ break;
+ }
+ }
+ return $extraPath;
+ }
+
+ /*
+ * Find the path name of first element in the array.
+ * If the array is empty, this will return the @extra interstitial element.
+ * If the specified path is not found or is not an array, it will return false.
+ */
+ function findFirstArrayElement( $path ) {
+ // Try for an ordinary element
+ foreach ( $this->pathInfo as $candidatePath => $info ) {
+ $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
+ $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
+ if ( $part1 == "$path/" && $part2 != '@' ) {
+ return $candidatePath;
+ }
+ }
+
+ // Try for an interstitial element
+ foreach ( $this->pathInfo as $candidatePath => $info ) {
+ $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
+ if ( $part1 == "$path/" ) {
+ return $candidatePath;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the indent string which sits after a given start position.
+ * Returns false if the position is not at the start of the line.
+ */
+ function getIndent( $pos, $key = false, $arrowPos = false ) {
+ $arrowIndent = ' ';
+ if ( $pos == 0 || $this->text[$pos-1] == "\n" ) {
+ $indentLength = strspn( $this->text, " \t", $pos );
+ $indent = substr( $this->text, $pos, $indentLength );
+ } else {
+ $indent = false;
+ }
+ if ( $indent !== false && $arrowPos !== false ) {
+ $textToInsert = "$indent$key ";
+ $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
+ if ( $arrowIndentLength > 0 ) {
+ $arrowIndent = str_repeat( ' ', $arrowIndentLength );
+ }
+ }
+ return array( $indent, $arrowIndent );
+ }
+
+ /**
+ * Run the parser on the text. Throws an exception if the string does not
+ * match our defined subset of PHP syntax.
+ */
+ public function parse() {
+ $this->initParse();
+ $this->pushState( 'file' );
+ $this->pushPath( '@extra-' . ($this->serial++) );
+ $token = $this->firstToken();
+
+ while ( !$token->isEnd() ) {
+ $state = $this->popState();
+ if ( !$state ) {
+ $this->error( 'internal error: empty state stack' );
+ }
+
+ switch ( $state ) {
+ case 'file':
+ $token = $this->expect( T_OPEN_TAG );
+ $token = $this->skipSpace();
+ if ( $token->isEnd() ) {
+ break 2;
+ }
+ $this->pushState( 'statement', 'file 2' );
+ break;
+ case 'file 2':
+ $token = $this->skipSpace();
+ if ( $token->isEnd() ) {
+ break 2;
+ }
+ $this->pushState( 'statement', 'file 2' );
+ break;
+ case 'statement':
+ $token = $this->skipSpace();
+ if ( !$this->validatePath( $token->text ) ) {
+ $this->error( "Invalid variable name \"{$token->text}\"" );
+ }
+ $this->nextPath( $token->text );
+ $this->expect( T_VARIABLE );
+ $this->skipSpace();
+ $arrayAssign = false;
+ if ( $this->currentToken()->type == '[' ) {
+ $this->nextToken();
+ $token = $this->skipSpace();
+ if ( !$token->isScalar() ) {
+ $this->error( "expected a string or number for the array key" );
+ }
+ if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
+ $text = $this->parseScalar( $token->text );
+ } else {
+ $text = $token->text;
+ }
+ if ( !$this->validatePath( $text ) ) {
+ $this->error( "Invalid associative array name \"$text\"" );
+ }
+ $this->pushPath( $text );
+ $this->nextToken();
+ $this->skipSpace();
+ $this->expect( ']' );
+ $this->skipSpace();
+ $arrayAssign = true;
+ }
+ $this->expect( '=' );
+ $this->skipSpace();
+ $this->startPathValue();
+ if ( $arrayAssign )
+ $this->pushState( 'expression', 'array assign end' );
+ else
+ $this->pushState( 'expression', 'statement end' );
+ break;
+ case 'array assign end':
+ case 'statement end':
+ $this->endPathValue();
+ if ( $state == 'array assign end' )
+ $this->popPath();
+ $this->skipSpace();
+ $this->expect( ';' );
+ $this->nextPath( '@extra-' . ($this->serial++) );
+ break;
+ case 'expression':
+ $token = $this->skipSpace();
+ if ( $token->type == T_ARRAY ) {
+ $this->pushState( 'array' );
+ } elseif ( $token->isScalar() ) {
+ $this->nextToken();
+ } elseif ( $token->type == T_VARIABLE ) {
+ $this->nextToken();
+ } else {
+ $this->error( "expected simple expression" );
+ }
+ break;
+ case 'array':
+ $this->skipSpace();
+ $this->expect( T_ARRAY );
+ $this->skipSpace();
+ $this->expect( '(' );
+ $this->skipSpace();
+ $this->pushPath( '@extra-' . ($this->serial++) );
+ if ( $this->isAhead( ')' ) ) {
+ // Empty array
+ $this->pushState( 'array end' );
+ } else {
+ $this->pushState( 'element', 'array end' );
+ }
+ break;
+ case 'array end':
+ $this->skipSpace();
+ $this->popPath();
+ $this->expect( ')' );
+ break;
+ case 'element':
+ $token = $this->skipSpace();
+ // Look ahead to find the double arrow
+ if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) {
+ // Found associative element
+ $this->pushState( 'assoc-element', 'element end' );
+ } else {
+ // Not associative
+ $this->nextPath( '@next' );
+ $this->startPathValue();
+ $this->pushState( 'expression', 'element end' );
+ }
+ break;
+ case 'element end':
+ $token = $this->skipSpace();
+ if ( $token->type == ',' ) {
+ $this->endPathValue();
+ $this->markComma();
+ $this->nextToken();
+ $this->nextPath( '@extra-' . ($this->serial++) );
+ // Look ahead to find ending bracket
+ if ( $this->isAhead( ")" ) ) {
+ // Found ending bracket, no continuation
+ $this->skipSpace();
+ } else {
+ // No ending bracket, continue to next element
+ $this->pushState( 'element' );
+ }
+ } elseif ( $token->type == ')' ) {
+ // End array
+ $this->endPathValue();
+ } else {
+ $this->error( "expected the next array element or the end of the array" );
+ }
+ break;
+ case 'assoc-element':
+ $token = $this->skipSpace();
+ if ( !$token->isScalar() ) {
+ $this->error( "expected a string or number for the array key" );
+ }
+ if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
+ $text = $this->parseScalar( $token->text );
+ } else {
+ $text = $token->text;
+ }
+ if ( !$this->validatePath( $text ) ) {
+ $this->error( "Invalid associative array name \"$text\"" );
+ }
+ $this->nextPath( $text );
+ $this->nextToken();
+ $this->skipSpace();
+ $this->markArrow();
+ $this->expect( T_DOUBLE_ARROW );
+ $this->skipSpace();
+ $this->startPathValue();
+ $this->pushState( 'expression' );
+ break;
+ }
+ }
+ if ( count( $this->stateStack ) ) {
+ $this->error( 'unexpected end of file' );
+ }
+ $this->popPath();
+ }
+
+ /**
+ * Initialise a parse.
+ */
+ protected function initParse() {
+ $this->tokens = token_get_all( $this->text );
+ $this->stateStack = array();
+ $this->pathStack = array();
+ $this->firstToken();
+ $this->pathInfo = array();
+ $this->serial = 1;
+ }
+
+ /**
+ * Set the parse position. Do not call this except from firstToken() and
+ * nextToken(), there is more to update than just the position.
+ */
+ protected function setPos( $pos ) {
+ $this->pos = $pos;
+ if ( $this->pos >= count( $this->tokens ) ) {
+ $this->currentToken = ConfEditorToken::newEnd();
+ } else {
+ $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] );
+ }
+ return $this->currentToken;
+ }
+
+ /**
+ * Create a ConfEditorToken from an element of token_get_all()
+ */
+ function newTokenObj( $internalToken ) {
+ if ( is_array( $internalToken ) ) {
+ return new ConfEditorToken( $internalToken[0], $internalToken[1] );
+ } else {
+ return new ConfEditorToken( $internalToken, $internalToken );
+ }
+ }
+
+ /**
+ * Reset the parse position
+ */
+ function firstToken() {
+ $this->setPos( 0 );
+ $this->prevToken = ConfEditorToken::newEnd();
+ $this->lineNum = 1;
+ $this->colNum = 1;
+ $this->byteNum = 0;
+ return $this->currentToken;
+ }
+
+ /**
+ * Get the current token
+ */
+ function currentToken() {
+ return $this->currentToken;
+ }
+
+ /**
+ * Advance the current position and return the resulting next token
+ */
+ function nextToken() {
+ if ( $this->currentToken ) {
+ $text = $this->currentToken->text;
+ $lfCount = substr_count( $text, "\n" );
+ if ( $lfCount ) {
+ $this->lineNum += $lfCount;
+ $this->colNum = strlen( $text ) - strrpos( $text, "\n" );
+ } else {
+ $this->colNum += strlen( $text );
+ }
+ $this->byteNum += strlen( $text );
+ }
+ $this->prevToken = $this->currentToken;
+ $this->setPos( $this->pos + 1 );
+ return $this->currentToken;
+ }
+
+ /**
+ * Get the token $offset steps ahead of the current position.
+ * $offset may be negative, to get tokens behind the current position.
+ */
+ function getTokenAhead( $offset ) {
+ $pos = $this->pos + $offset;
+ if ( $pos >= count( $this->tokens ) || $pos < 0 ) {
+ return ConfEditorToken::newEnd();
+ } else {
+ return $this->newTokenObj( $this->tokens[$pos] );
+ }
+ }
+
+ /**
+ * Advances the current position past any whitespace or comments
+ */
+ function skipSpace() {
+ while ( $this->currentToken && $this->currentToken->isSkip() ) {
+ $this->nextToken();
+ }
+ return $this->currentToken;
+ }
+
+ /**
+ * Throws an error if the current token is not of the given type, and
+ * then advances to the next position.
+ */
+ function expect( $type ) {
+ if ( $this->currentToken && $this->currentToken->type == $type ) {
+ return $this->nextToken();
+ } else {
+ $this->error( "expected " . $this->getTypeName( $type ) .
+ ", got " . $this->getTypeName( $this->currentToken->type ) );
+ }
+ }
+
+ /**
+ * Push a state or two on to the state stack.
+ */
+ function pushState( $nextState, $stateAfterThat = null ) {
+ if ( $stateAfterThat !== null ) {
+ $this->stateStack[] = $stateAfterThat;
+ }
+ $this->stateStack[] = $nextState;
+ }
+
+ /**
+ * Pop a state from the state stack.
+ */
+ function popState() {
+ return array_pop( $this->stateStack );
+ }
+
+ /**
+ * Returns true if the user input path is valid.
+ * This exists to allow "/" and "@" to be reserved for string path keys
+ */
+ function validatePath( $path ) {
+ return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
+ }
+
+ /**
+ * Internal function to update some things at the end of a path region. Do
+ * not call except from popPath() or nextPath().
+ */
+ function endPath() {
+ $i = count( $this->pathStack ) - 1;
+ $key = '';
+ foreach ( $this->pathStack as $pathInfo ) {
+ if ( $key !== '' ) {
+ $key .= '/';
+ }
+ $key .= $pathInfo['name'];
+ }
+ $pathInfo['endByte'] = $this->byteNum;
+ $pathInfo['endToken'] = $this->pos;
+ $this->pathInfo[$key] = $pathInfo;
+ }
+
+ /**
+ * Go up to a new path level, for example at the start of an array.
+ */
+ function pushPath( $path ) {
+ $this->pathStack[] = array(
+ 'name' => $path,
+ 'level' => count( $this->pathStack ) + 1,
+ 'startByte' => $this->byteNum,
+ 'startToken' => $this->pos,
+ 'valueStartToken' => false,
+ 'valueStartByte' => false,
+ 'valueEndToken' => false,
+ 'valueEndByte' => false,
+ 'nextArrayIndex' => 0,
+ 'hasComma' => false,
+ 'arrowByte' => false
+ );
+ }
+
+ /**
+ * Go down a path level, for example at the end of an array.
+ */
+ function popPath() {
+ $this->endPath();
+ array_pop( $this->pathStack );
+ }
+
+ /**
+ * Go to the next path on the same level. This ends the current path and
+ * starts a new one. If $path is @next, the new path is set to the next
+ * numeric array element.
+ */
+ function nextPath( $path ) {
+ $this->endPath();
+ $i = count( $this->pathStack ) - 1;
+ if ( $path == '@next' ) {
+ $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex'];
+ $this->pathStack[$i]['name'] = $nextArrayIndex;
+ $nextArrayIndex++;
+ } else {
+ $this->pathStack[$i]['name'] = $path;
+ }
+ $this->pathStack[$i] =
+ array(
+ 'startByte' => $this->byteNum,
+ 'startToken' => $this->pos,
+ 'valueStartToken' => false,
+ 'valueStartByte' => false,
+ 'valueEndToken' => false,
+ 'valueEndByte' => false,
+ 'hasComma' => false,
+ 'arrowByte' => false,
+ ) + $this->pathStack[$i];
+ }
+
+ /**
+ * Mark the start of the value part of a path.
+ */
+ function startPathValue() {
+ $path =& $this->pathStack[count( $this->pathStack ) - 1];
+ $path['valueStartToken'] = $this->pos;
+ $path['valueStartByte'] = $this->byteNum;
+ }
+
+ /**
+ * Mark the end of the value part of a path.
+ */
+ function endPathValue() {
+ $path =& $this->pathStack[count( $this->pathStack ) - 1];
+ $path['valueEndToken'] = $this->pos;
+ $path['valueEndByte'] = $this->byteNum;
+ }
+
+ /**
+ * Mark the comma separator in an array element
+ */
+ function markComma() {
+ $path =& $this->pathStack[count( $this->pathStack ) - 1];
+ $path['hasComma'] = true;
+ }
+
+ /**
+ * Mark the arrow separator in an associative array element
+ */
+ function markArrow() {
+ $path =& $this->pathStack[count( $this->pathStack ) - 1];
+ $path['arrowByte'] = $this->byteNum;
+ }
+
+ /**
+ * Generate a parse error
+ */
+ function error( $msg ) {
+ throw new ConfEditorParseError( $this, $msg );
+ }
+
+ /**
+ * Get a readable name for the given token type.
+ */
+ function getTypeName( $type ) {
+ if ( is_int( $type ) ) {
+ return token_name( $type );
+ } else {
+ return "\"$type\"";
+ }
+ }
+
+ /**
+ * Looks ahead to see if the given type is the next token type, starting
+ * from the current position plus the given offset. Skips any intervening
+ * whitespace.
+ */
+ function isAhead( $type, $offset = 0 ) {
+ $ahead = $offset;
+ $token = $this->getTokenAhead( $offset );
+ while ( !$token->isEnd() ) {
+ if ( $token->isSkip() ) {
+ $ahead++;
+ $token = $this->getTokenAhead( $ahead );
+ continue;
+ } elseif ( $token->type == $type ) {
+ // Found the type
+ return true;
+ } else {
+ // Not found
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the previous token object
+ */
+ function prevToken() {
+ return $this->prevToken;
+ }
+
+ /**
+ * Echo a reasonably readable representation of the tokenizer array.
+ */
+ function dumpTokens() {
+ $out = '';
+ foreach ( $this->tokens as $token ) {
+ $obj = $this->newTokenObj( $token );
+ $out .= sprintf( "%-28s %s\n",
+ $this->getTypeName( $obj->type ),
+ addcslashes( $obj->text, "\0..\37" ) );
+ }
+ echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
+ }
+}
+
+/**
+ * Exception class for parse errors
+ */
+class ConfEditorParseError extends MWException {
+ var $lineNum, $colNum;
+ function __construct( $editor, $msg ) {
+ $this->lineNum = $editor->lineNum;
+ $this->colNum = $editor->colNum;
+ parent::__construct( "Parse error on line {$editor->lineNum} " .
+ "col {$editor->colNum}: $msg" );
+ }
+
+ function highlight( $text ) {
+ $lines = StringUtils::explode( "\n", $text );
+ foreach ( $lines as $lineNum => $line ) {
+ if ( $lineNum == $this->lineNum - 1 ) {
+ return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n";
+ }
+ }
+ }
+
+}
+
+/**
+ * Class to wrap a token from the tokenizer.
+ */
+class ConfEditorToken {
+ var $type, $text;
+
+ static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING );
+ static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT );
+
+ static function newEnd() {
+ return new self( 'END', '' );
+ }
+
+ function __construct( $type, $text ) {
+ $this->type = $type;
+ $this->text = $text;
+ }
+
+ function isSkip() {
+ return in_array( $this->type, self::$skipTypes );
+ }
+
+ function isScalar() {
+ return in_array( $this->type, self::$scalarTypes );
+ }
+
+ function isEnd() {
+ return $this->type == 'END';
+ }
+}
+
diff --git a/includes/Credits.php b/includes/Credits.php
index ae9377f2..91ba3f16 100644
--- a/includes/Credits.php
+++ b/includes/Credits.php
@@ -55,13 +55,13 @@ class Credits {
* @param $showIfMax Bool: whether to contributors if there more than $cnt
* @return String: html
*/
- public static function getCredits($article, $cnt, $showIfMax=true) {
+ public static function getCredits( Article $article, $cnt, $showIfMax = true ) {
wfProfileIn( __METHOD__ );
$s = '';
if( isset( $cnt ) && $cnt != 0 ){
$s = self::getAuthor( $article );
- if ($cnt > 1 || $cnt < 0) {
+ if ( $cnt > 1 || $cnt < 0 ) {
$s .= ' ' . self::getContributors( $article, $cnt - 1, $showIfMax );
}
}
@@ -75,7 +75,7 @@ class Credits {
* @param $article Article object
*/
protected static function getAuthor( Article $article ){
- global $wgLang, $wgAllowRealName;
+ global $wgLang;
$user = User::newFromId( $article->getUser() );
@@ -87,7 +87,7 @@ class Credits {
$d = '';
$t = '';
}
- return wfMsg( 'lastmodifiedatby', $d, $t, self::userLink( $user ) );
+ return wfMsgExt( 'lastmodifiedatby', 'parsemag', $d, $t, self::userLink( $user ), $user->getName() );
}
/**
@@ -98,11 +98,11 @@ class Credits {
* @return String: html
*/
protected static function getContributors( Article $article, $cnt, $showIfMax ) {
- global $wgLang, $wgAllowRealName;
+ global $wgLang, $wgHiddenPrefs;
$contributors = $article->getContributors();
- $others_link = '';
+ $others_link = false;
# Hmm... too many to fit!
if( $cnt > 0 && $contributors->count() > $cnt ){
@@ -113,38 +113,48 @@ class Credits {
$real_names = array();
$user_names = array();
- $anon = 0;
+ $anon_ips = array();
# Sift for real versus user names
foreach( $contributors as $user ) {
$cnt--;
if( $user->isLoggedIn() ){
$link = self::link( $user );
- if( $wgAllowRealName && $user->getRealName() )
+ if( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() )
$real_names[] = $link;
else
$user_names[] = $link;
} else {
- $anon++;
+ $anon_ips[] = self::link( $user );
}
if( $cnt == 0 ) break;
}
- # Two strings: real names, and user names
- $real = $wgLang->listToText( $real_names );
- $user = $wgLang->listToText( $user_names );
- if( $anon )
- $anon = wfMsgExt( 'anonymous', array( 'parseinline' ), $anon );
+ if ( count( $real_names ) ) {
+ $real = $wgLang->listToText( $real_names );
+ } else {
+ $real = false;
+ }
# "ThisSite user(s) A, B and C"
- if( !empty( $user ) ){
- $user = wfMsgExt( 'siteusers', array( 'parsemag' ), $user, count( $user_names ) );
+ if( count( $user_names ) ){
+ $user = wfMsgExt( 'siteusers', array( 'parsemag' ),
+ $wgLang->listToText( $user_names ), count( $user_names ) );
+ } else {
+ $user = false;
+ }
+
+ if( count( $anon_ips ) ){
+ $anon = wfMsgExt( 'anonusers', array( 'parsemag' ),
+ $wgLang->listToText( $anon_ips ), count( $anon_ips ) );
+ } else {
+ $anon = false;
}
# This is the big list, all mooshed together. We sift for blank strings
$fulllist = array();
foreach( array( $real, $user, $anon, $others_link ) as $s ){
- if( !empty( $s ) ){
+ if( $s ){
array_push( $fulllist, $s );
}
}
@@ -153,40 +163,42 @@ class Credits {
$creds = $wgLang->listToText( $fulllist );
# "Based on work by ..."
- return empty( $creds ) ? '' : wfMsg( 'othercontribs', $creds );
+ return strlen( $creds ) ? wfMsg( 'othercontribs', $creds ) : '';
}
/**
- * Get a link to $user_name page
+ * Get a link to $user's user page
* @param $user User object
* @return String: html
*/
protected static function link( User $user ) {
- global $wgUser, $wgAllowRealName;
- if( $wgAllowRealName )
+ global $wgUser, $wgHiddenPrefs;
+ if( !in_array( 'realname', $wgHiddenPrefs ) && !$user->isAnon() )
$real = $user->getRealName();
else
$real = false;
$skin = $wgUser->getSkin();
- $page = $user->getUserPage();
-
+ $page = $user->isAnon() ?
+ SpecialPage::getTitleFor( 'Contributions', $user->getName() ) :
+ $user->getUserPage();
+
return $skin->link( $page, htmlspecialchars( $real ? $real : $user->getName() ) );
}
/**
- * Get a link to $user_name page
+ * Get a link to $user's user page
* @param $user_name String: user name
* @param $linkText String: optional display
* @return String: html
*/
protected static function userLink( User $user ) {
- global $wgUser, $wgAllowRealName;
+ $link = self::link( $user );
if( $user->isAnon() ){
- return wfMsgExt( 'anonymous', array( 'parseinline' ), 1 );
+ return wfMsgExt( 'anonuser', array( 'parseinline', 'replaceafter' ), $link );
} else {
- $link = self::link( $user );
- if( $wgAllowRealName && $user->getRealName() )
+ global $wgHiddenPrefs;
+ if( !in_array( 'realname', $wgHiddenPrefs ) && $user->getRealName() )
return $link;
else
return wfMsgExt( 'siteuser', array( 'parseinline', 'replaceafter' ), $link );
@@ -203,4 +215,4 @@ class Credits {
$skin = $wgUser->getSkin();
return $skin->link( $article->getTitle(), wfMsgHtml( 'others' ), array(), array( 'action' => 'credits' ), array( 'known' ) );
}
-} \ No newline at end of file
+}
diff --git a/includes/DatabaseFunctions.php b/includes/DatabaseFunctions.php
index 52e9a8c8..2df56115 100644
--- a/includes/DatabaseFunctions.php
+++ b/includes/DatabaseFunctions.php
@@ -58,7 +58,7 @@ function wfIgnoreSQLErrors( $newstate, $dbi = DB_LAST ) {
if ( $db !== false ) {
return $db->ignoreErrors( $newstate );
} else {
- return NULL;
+ return null;
}
}
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 160273b8..a369fccd 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -33,7 +33,7 @@ if ( !defined( 'MW_PHP4' ) ) {
}
/** MediaWiki version number */
-$wgVersion = '1.15.5';
+$wgVersion = '1.16.0';
/** Name of the site. It must be changed in LocalSettings.php */
$wgSitename = 'MediaWiki';
@@ -142,16 +142,17 @@ $wgRedirectScript = false; ///< defaults to "{$wgScriptPath}/redirect{$wgScrip
* splitting style sheets or images outside the main document root.
*/
/**
- * style path as seen by users
+ * asset paths as seen by users
*/
$wgStylePath = false; ///< defaults to "{$wgScriptPath}/skins"
+$wgExtensionAssetsPath = false; ///< defaults to "{$wgScriptPath}/extensions"
+
/**
* filesystem stylesheets directory
*/
$wgStyleDirectory = false; ///< defaults to "{$IP}/skins"
$wgStyleSheetPath = &$wgStylePath;
$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"
$wgHashedUploadDirectory = true;
@@ -165,6 +166,16 @@ $wgUploadBaseUrl = "";
/**@}*/
/**
+ * Directory for caching data in the local filesystem. Should not be accessible
+ * from the web. Set this to false to not use any local caches.
+ *
+ * Note: if multiple wikis share the same localisation cache directory, they
+ * must all have the same set of extensions. You can set a directory just for
+ * the localisation cache using $wgLocalisationCacheConf['storeDirectory'].
+ */
+$wgCacheDirectory = false;
+
+/**
* Default value for chmoding of new directories.
*/
$wgDirectoryMode = 0777;
@@ -181,11 +192,14 @@ $wgFileStore['deleted']['directory'] = false;///< Defaults to $wgUploadDirectory
$wgFileStore['deleted']['url'] = null; ///< Private
$wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split
+$wgImgAuthDetails = false; ///< defaults to false - only set to true if you use img_auth and want the user to see details on why access failed
+$wgImgAuthPublicTest = true; ///< defaults to true - if public read is turned on, no need for img_auth, config error unless other access is used
+
/**@{
* File repository structures
*
- * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepo is
- * a an array of such structures. Each repository structure is an associative
+ * $wgLocalFileRepo is a single repository structure, and $wgForeignFileRepos is
+ * an array of such structures. Each repository structure is an associative
* array of properties configuring the repository.
*
* Properties required for all repos:
@@ -194,20 +208,27 @@ $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split
*
* name A unique name for the repository.
*
- * For all core repos:
+ * For most core repos:
* url Base public URL
* hashLevels The number of directory levels for hash-based division of files
* thumbScriptUrl The URL for thumb.php (optional, not recommended)
* transformVia404 Whether to skip media file transformation on parse and rely on a 404
* handler instead.
- * initialCapital Equivalent to $wgCapitalLinks, determines whether filenames implicitly
- * start with a capital letter. The current implementation may give incorrect
- * description page links when the local $wgCapitalLinks and initialCapital
- * are mismatched.
+ * initialCapital Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE],
+ * determines whether filenames implicitly start with a capital letter.
+ * The current implementation may give incorrect description page links
+ * when the local $wgCapitalLinks and initialCapital are mismatched.
* pathDisclosureProtection
* May be 'paranoid' to remove all parameters from error messages, 'none' to
* leave the paths in unchanged, or 'simple' to replace paths with
* placeholders. Default for LocalRepo is 'simple'.
+ * fileMode This allows wikis to set the file mode when uploading/moving files. Default
+ * is 0644.
+ * directory The local filesystem directory where public files are stored. Not used for
+ * some remote repos.
+ * thumbDir The base thumbnail directory. Defaults to <directory>/thumb.
+ * thumbUrl The base thumbnail URL. Defaults to <url>/thumb.
+ *
*
* These settings describe a foreign MediaWiki installation. They are optional, and will be ignored
* for local repositories:
@@ -224,7 +245,7 @@ $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split
* equivalent to the corresponding member of $wgDBservers
* tablePrefix Table prefix, the foreign wiki's $wgDBprefix
* hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc
- *
+ *
* ForeignAPIRepo:
* apibase Use for the foreign API's URL
* apiThumbCacheExpiry How long to locally cache thumbs for
@@ -237,6 +258,13 @@ $wgForeignFileRepos = array();
/**@}*/
/**
+ * Use Commons as a remote file repository. Essentially a wrapper, when this
+ * is enabled $wgForeignFileRepos will point at Commons with a set of default
+ * settings
+ */
+$wgUseInstantCommons = false;
+
+/**
* Allowed title characters -- regex character class
* Don't change this unless you know what you're doing
*
@@ -263,6 +291,7 @@ $wgForeignFileRepos = array();
* this breaks interlanguage links
*/
$wgLegalTitleChars = " %!\"$&'()*,\\-.\\/0-9:;=?@A-Z\\\\^_`a-z~\\x80-\\xFF+";
+$wgIllegalFileChars = ":"; // These are additional characters that should be replaced with '-' in file names
/**
@@ -285,7 +314,7 @@ $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.
*/
-$wgAntivirus= NULL;
+$wgAntivirus= null;
/** Configuration for different virus scanners. This an associative array of associative arrays:
* it contains on setup array per known scanner type. The entry is selected by $wgAntivirus, i.e.
@@ -352,11 +381,11 @@ $wgVerifyMimeType= true;
/** 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.
+#$wgMimeTypeFile= null; #use built-in defaults only.
/** Sets the mime type info file to use by MimeMagic.php. */
$wgMimeInfoFile= "includes/mime.info";
-#$wgMimeInfoFile= NULL; #use built-in defaults only.
+#$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
@@ -369,7 +398,7 @@ $wgLoadFileinfoExtension= false;
* 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= 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
@@ -426,8 +455,12 @@ $wgSharedUploadDBname = false;
$wgSharedUploadDBprefix = '';
/** Cache shared metadata in memcached. Don't do this if the commons wiki is in a different memcached domain */
$wgCacheSharedUploads = true;
-/** Allow for upload to be copied from an URL. Requires Special:Upload?source=web */
+/**
+* Allow for upload to be copied from an URL. Requires Special:Upload?source=web
+* The timeout for copy uploads is set by $wgHTTPTimeout.
+*/
$wgAllowCopyUploads = false;
+
/**
* Max size for uploads, in bytes. Currently only works for uploads from URL
* via CURL (see $wgAllowCopyUploads). The only way to impose limits on
@@ -440,6 +473,9 @@ $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';
+ *
+ * This also affects images inline images that do not exist. In that case the URL will get
+ * (?|&)wpDestFile=<filename> appended to it as appropriate.
*/
$wgUploadNavigationUrl = false;
@@ -564,6 +600,10 @@ $wgDBpassword = '';
/** Database type */
$wgDBtype = 'mysql';
+/** Separate username and password for maintenance tasks. Leave as null to use the default */
+$wgDBadminuser = null;
+$wgDBadminpassword = null;
+
/** Search type
* Leave as null to select the default search engine for the
* selected database type (eg SearchMySQL), or set to a class
@@ -610,6 +650,8 @@ $wgCheckDBSchema = true;
* 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.
+ * The user_properties table is also added so that users will continue to have their
+ * preferences shared (preferences were stored in the user table prior to 1.16)
*
* $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
@@ -618,7 +660,7 @@ $wgCheckDBSchema = true;
*/
$wgSharedDB = null;
$wgSharedPrefix = false; # Defaults to $wgDBprefix
-$wgSharedTables = array( 'user' );
+$wgSharedTables = array( 'user', 'user_properties' );
/**
* Database load balancer
@@ -732,8 +774,17 @@ $wgParserCacheType = CACHE_ANYTHING;
$wgParserCacheExpireTime = 86400;
+// Select which DBA handler <http://www.php.net/manual/en/dba.requirements.php> to use as CACHE_DBA backend
+$wgDBAhandler = 'db3';
+
$wgSessionsInMemcached = false;
+/** This is used for setting php's session.save_handler. In practice, you will
+ * almost never need to change this ever. Other options might be 'user' or
+ * 'session_mysql.' Setting to null skips setting this entirely (which might be
+ * useful if you're doing cross-application sessions, see bug 11381) */
+$wgSessionHandler = 'files';
+
/**@{
* Memcached-specific settings
* See docs/memcached.txt
@@ -742,12 +793,15 @@ $wgUseMemCached = false;
$wgMemCachedDebug = false; ///< Will be set to false in Setup.php, if the server isn't working
$wgMemCachedServers = array( '127.0.0.1:11000' );
$wgMemCachedPersistent = false;
+$wgMemCachedTimeout = 100000; //Data timeout in microseconds
/**@}*/
/**
- * Directory for local copy of message cache, for use in addition to memcached
+ * Set this to true to make a local copy of the message cache, for use in
+ * addition to memcached. The files will be put in $wgCacheDirectory.
*/
-$wgLocalMessageCache = false;
+$wgUseLocalMessageCache = false;
+
/**
* Defines format of local cache
* true - Serialized object
@@ -755,6 +809,34 @@ $wgLocalMessageCache = false;
*/
$wgLocalMessageCacheSerialized = true;
+/**
+ * Localisation cache configuration. Associative array with keys:
+ * class: The class to use. May be overridden by extensions.
+ *
+ * store: The location to store cache data. May be 'files', 'db' or
+ * 'detect'. If set to "files", data will be in CDB files. If set
+ * to "db", data will be stored to the database. If set to
+ * "detect", files will be used if $wgCacheDirectory is set,
+ * otherwise the database will be used.
+ *
+ * storeClass: The class name for the underlying storage. If set to a class
+ * name, it overrides the "store" setting.
+ *
+ * storeDirectory: If the store class puts its data in files, this is the
+ * directory it will use. If this is false, $wgCacheDirectory
+ * will be used.
+ *
+ * manualRecache: Set this to true to disable cache updates on web requests.
+ * Use maintenance/rebuildLocalisationCache.php instead.
+ */
+$wgLocalisationCacheConf = array(
+ 'class' => 'LocalisationCache',
+ 'store' => 'detect',
+ 'storeClass' => false,
+ 'storeDirectory' => false,
+ 'manualRecache' => false,
+);
+
# Language settings
#
/** Site language code, should be one of ./languages/Language(.*).php */
@@ -776,14 +858,42 @@ $wgHideInterlanguageLinks = false;
/** List of language names or overrides for default names in Names.php */
$wgExtraLanguageNames = array();
+/**
+ * List of language codes that don't correspond to an actual language.
+ * These codes are leftoffs from renames, or other legacy things.
+ * Also, qqq is a dummy "language" for documenting messages.
+ */
+$wgDummyLanguageCodes = array( 'qqq', 'als', 'be-x-old', 'dk', 'fiu-vro', 'iu', 'nb', 'simple', 'tp' );
+
/** We speak UTF-8 all the time now, unless some oddities happen */
$wgInputEncoding = 'UTF-8';
$wgOutputEncoding = 'UTF-8';
$wgEditEncoding = '';
/**
+ * Set this to true to replace Arabic presentation forms with their standard
+ * forms in the U+0600-U+06FF block. This only works if $wgLanguageCode is
+ * set to "ar".
+ *
+ * Note that pages with titles containing presentation forms will become
+ * inaccessible, run maintenance/cleanupTitles.php to fix this.
+ */
+$wgFixArabicUnicode = true;
+
+/**
+ * Set this to true to replace ZWJ-based chillu sequences in Malayalam text
+ * with their Unicode 5.1 equivalents. This only works if $wgLanguageCode is
+ * set to "ml". Note that some clients (even new clients as of 2010) do not
+ * support these characters.
+ *
+ * If you enable this on an existing wiki, run maintenance/cleanupTitles.php to
+ * fix any ZWJ sequences in existing page titles.
+ */
+$wgFixMalayalamUnicode = true;
+
+/**
* Locale for LC_CTYPE, to work around http://bugs.php.net/bug.php?id=45132
- * For Unix-like operating systems, set this to to a locale that has a UTF-8
+ * For Unix-like operating systems, set this to to a locale that has a UTF-8
* character set. Only the character set is relevant.
*/
$wgShellLocale = 'en_US.utf8';
@@ -817,11 +927,54 @@ $wgLegacyEncoding = false;
*/
$wgLegacySchemaConversion = false;
-$wgMimeType = 'text/html';
-$wgJsMimeType = 'text/javascript';
-$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';
+$wgMimeType = 'text/html';
+$wgJsMimeType = 'text/javascript';
+$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';
+
+/**
+ * Should we output an HTML5 doctype? This mode is still experimental, but
+ * all indications are that it should be usable, so it's enabled by default.
+ * If all goes well, it will be removed and become always true before the 1.16
+ * release.
+ */
+$wgHtml5 = true;
+
+/**
+ * Defines the value of the version attribute in the &lt;html&gt; tag, if any.
+ * Will be initialized later if not set explicitly.
+ */
+$wgHtml5Version = null;
+
+/**
+ * Enabled RDFa attributes for use in wikitext.
+ * NOTE: Interaction with HTML5 is somewhat underspecified.
+ */
+$wgAllowRdfaAttributes = false;
+
+/**
+ * Enabled HTML5 microdata attributes for use in wikitext, if $wgHtml5 is also true.
+ */
+$wgAllowMicrodataAttributes = false;
+
+/**
+ * Should we try to make our HTML output well-formed XML? If set to false,
+ * output will be a few bytes shorter, and the HTML will arguably be more
+ * readable. If set to true, life will be much easier for the authors of
+ * screen-scraping bots, and the HTML will arguably be more readable.
+ *
+ * Setting this to false may omit quotation marks on some attributes, omit
+ * slashes from some self-closing tags, omit some ending tags, etc., where
+ * permitted by HTML5. Setting it to true will not guarantee that all pages
+ * will be well-formed, although non-well-formed pages should be rare and it's
+ * a bug if you find one. Conversely, setting it to false doesn't mean that
+ * all XML-y constructs will be omitted, just that they might be.
+ *
+ * Because of compatibility with screen-scraping bots, and because it's
+ * controversial, this is currently left to true by default.
+ */
+$wgWellFormedXml = true;
/**
* Permit other namespaces in addition to the w3.org default.
@@ -831,7 +984,7 @@ $wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
* Normally we wouldn't have to define this in the root <html>
* element, but IE needs it there in some circumstances.
*/
-$wgXhtmlNamespaces = array();
+$wgXhtmlNamespaces = array();
/** Enable to allow rewriting dates in page text.
* DOES NOT FORMAT CORRECTLY FOR MOST LANGUAGES */
@@ -885,6 +1038,32 @@ $wgDisableTitleConversion = false;
/** Default variant code, if false, the default will be the language code */
$wgDefaultLanguageVariant = false;
+/** Disabled variants array of language variant conversion.
+ * example:
+ * $wgDisabledVariants[] = 'zh-mo';
+ * $wgDisabledVariants[] = 'zh-my';
+ *
+ * or:
+ * $wgDisabledVariants = array('zh-mo', 'zh-my');
+ */
+$wgDisabledVariants = array();
+
+/**
+ * Like $wgArticlePath, but on multi-variant wikis, this provides a
+ * path format that describes which parts of the URL contain the
+ * language variant. For Example:
+ *
+ * $wgLanguageCode = 'sr';
+ * $wgVariantArticlePath = '/$2/$1';
+ * $wgArticlePath = '/wiki/$1';
+ *
+ * A link to /wiki/ would be redirected to /sr/Главна_страна
+ *
+ * It is important that $wgArticlePath not overlap with possible values
+ * of $wgVariantArticlePath.
+ */
+$wgVariantArticlePath = false;///< defaults to false
+
/**
* Show a bar of language selection links in the user login and user
* registration forms; edit the "loginlanguagelinks" message to
@@ -970,11 +1149,11 @@ $wgExtraSubtitle = '';
$wgSiteSupportPage = ''; # A page where you users can receive donations
/**
- * Set this to a string to put the wiki into read-only mode. The text will be
- * used as an explanation to users.
+ * Set this to a string to put the wiki into read-only mode. The text will be
+ * used as an explanation to users.
*
- * This prevents most write operations via the web interface. Cache updates may
- * still be possible. To prevent database writes completely, use the read_only
+ * This prevents most write operations via the web interface. Cache updates may
+ * still be possible. To prevent database writes completely, use the read_only
* option in MySQL.
*/
$wgReadOnly = null;
@@ -989,7 +1168,7 @@ $wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBg
/**
* Filename for debug logging. See http://www.mediawiki.org/wiki/How_to_debug
* The debug log file should be not be publicly accessible if it is used, as it
- * may contain private data.
+ * may contain private data.
*/
$wgDebugLogFile = '';
@@ -999,14 +1178,14 @@ $wgDebugLogFile = '';
$wgDebugLogPrefix = '';
/**
- * If true, instead of redirecting, show a page with a link to the redirect
+ * If true, instead of redirecting, show a page with a link to the redirect
* destination. This allows for the inspection of PHP error messages, and easy
* resubmission of form data. For developer use only.
*/
$wgDebugRedirects = false;
/**
- * If true, log debugging data from action=raw.
+ * If true, log debugging data from action=raw.
* This is normally false to avoid overlapping debug entries due to gen=css and
* gen=js requests.
*/
@@ -1017,14 +1196,11 @@ $wgDebugRawPage = false;
*
* This may occasionally be useful when supporting a non-technical end-user. It's
* more secure than exposing the debug log file to the web, since the output only
- * contains private data for the current user. But it's not ideal for development
+ * contains private data for the current user. But it's not ideal for development
* use since data is lost on fatal errors and redirects.
*/
$wgDebugComments = false;
-/** Does nothing. Obsolete? */
-$wgLogQueries = false;
-
/**
* Write SQL queries to the debug log
*/
@@ -1046,6 +1222,16 @@ $wgDebugLogGroups = array();
$wgShowDebug = false;
/**
+ * Prefix debug messages with relative timestamp. Very-poor man's profiler.
+ */
+$wgDebugTimestamps = false;
+
+/**
+ * Print HTTP headers for every request in the debug information.
+ */
+$wgDebugPrintHttpHeaders = true;
+
+/**
* Show the contents of $wgHooks in Special:Version
*/
$wgSpecialVersionShowHooks = false;
@@ -1073,22 +1259,33 @@ $wgColorErrors = true;
$wgShowExceptionDetails = false;
/**
+ * If true, show a backtrace for database errors
+ */
+$wgShowDBErrorBacktrace = false;
+
+/**
* Expose backend server host names through the API and various HTML comments
*/
$wgShowHostnames = false;
/**
+ * If set to true MediaWiki will throw notices for some possible error
+ * conditions and for deprecated functions.
+ */
+$wgDevelopmentWarnings = false;
+
+/**
* Use experimental, DMOZ-like category browser
*/
$wgUseCategoryBrowser = false;
/**
- * Keep parsed pages in a cache (objectcache table, turck, or memcached)
+ * Keep parsed pages in a cache (objectcache table or memcached)
* to speed up output of the same page viewed by another user with the
* same options.
*
* This can provide a significant speedup for medium to large pages,
- * so you probably want to keep it on. Extensions that conflict with the
+ * so you probably want to keep it on. Extensions that conflict with the
* parser cache should disable the cache on a per-page basis instead.
*/
$wgEnableParserCache = true;
@@ -1120,7 +1317,7 @@ $wgSidebarCacheExpiry = 86400;
* as a valid article? If $wgUseCommaCount is set to true, it will be
* counted if it contains at least one comma. If it is set to false
* (default), it will only be counted if it contains at least one [[wiki
- * link]]. See http://meta.wikimedia.org/wiki/Help:Article_count
+ * link]]. See http://www.mediawiki.org/wiki/Manual:Article_count
*
* Retroactively changing this variable will not affect
* the existing count (cf. maintenance/recount.sql).
@@ -1142,6 +1339,19 @@ $wgSysopRangeBans = true; # Allow sysops to ban IP ranges
$wgAutoblockExpiry = 86400; # Number of seconds before autoblock entries expire
$wgBlockAllowsUTEdit = false; # Default setting for option on block form to allow self talkpage editing whilst blocked
$wgSysopEmailBans = true; # Allow sysops to ban users from accessing Emailuser
+$wgBlockCIDRLimit = array(
+ 'IPv4' => 16, # Blocks larger than a /16 (64k addresses) will not be allowed
+ 'IPv6' => 64, # 2^64 = ~1.8x10^19 addresses
+);
+
+/**
+ * If true, blocked users will not be allowed to login. When using this with
+ * a public wiki, the effect of logging out blocked users may actually be
+ * avers: unless the user's address is also blocked (e.g. auto-block),
+ * logging the user out will again allow reading and editing, just as for
+ * anonymous visitors.
+ */
+$wgBlockDisablesLogin = false; #
# Pages anonymous user may see as an array, e.g.:
# array ( "Main Page", "Wikipedia:Help");
@@ -1186,6 +1396,7 @@ $wgGroupPermissions['*']['edit'] = true;
$wgGroupPermissions['*']['createpage'] = true;
$wgGroupPermissions['*']['createtalk'] = true;
$wgGroupPermissions['*']['writeapi'] = true;
+//$wgGroupPermissions['*']['patrolmarks'] = false; // let anons see what was patrolled
// Implicit group for all logged-in accounts
$wgGroupPermissions['user']['move'] = true;
@@ -1202,6 +1413,7 @@ $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']['sendemail'] = true;
// Implicit group for accounts that pass $wgAutoConfirmAge
$wgGroupPermissions['autoconfirmed']['autoconfirmed'] = true;
@@ -1223,9 +1435,11 @@ $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']['deletedtext'] = true; // can view deleted revision text
$wgGroupPermissions['sysop']['undelete'] = true;
$wgGroupPermissions['sysop']['editinterface'] = true;
-$wgGroupPermissions['sysop']['editusercssjs'] = true;
+$wgGroupPermissions['sysop']['editusercss'] = true;
+$wgGroupPermissions['sysop']['edituserjs'] = true;
$wgGroupPermissions['sysop']['import'] = true;
$wgGroupPermissions['sysop']['importupload'] = true;
$wgGroupPermissions['sysop']['move'] = true;
@@ -1249,6 +1463,7 @@ $wgGroupPermissions['sysop']['markbotedits'] = true;
$wgGroupPermissions['sysop']['apihighlimits'] = true;
$wgGroupPermissions['sysop']['browsearchive'] = true;
$wgGroupPermissions['sysop']['noratelimit'] = true;
+$wgGroupPermissions['sysop']['versiondetail'] = true;
$wgGroupPermissions['sysop']['movefile'] = true;
#$wgGroupPermissions['sysop']['mergehistory'] = true;
@@ -1276,6 +1491,15 @@ $wgGroupPermissions['bureaucrat']['noratelimit'] = true;
*/
# $wgGroupPermissions['developer']['siteadmin'] = true;
+/**
+ * Permission keys revoked from users in each group.
+ * This acts the same way as wgGroupPermissions above, except that
+ * if the user is in a group here, the permission will be removed from them.
+ *
+ * Improperly setting this could mean that your users will be unable to perform
+ * certain essential tasks, so use at your own risk!
+ */
+$wgRevokePermissions = array();
/**
* Implicit groups, aren't shown on Special:Listusers or somewhere else
@@ -1287,7 +1511,7 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
* are allowed to add or revoke.
*
* Setting the list of groups to add or revoke to true is equivalent to "any group".
- *
+ *
* For example, to allow sysops to add themselves to the "bot" group:
*
* $wgGroupsAddToSelf = array( 'sysop' => array( 'bot' ) );
@@ -1298,7 +1522,7 @@ $wgImplicitGroups = array( '*', 'user', 'autoconfirmed' );
*
* This allows users in the '*' group (i.e. any user) to remove themselves from
* any group that they happen to be in.
- *
+ *
*/
$wgGroupsAddToSelf = array();
$wgGroupsRemoveFromSelf = array();
@@ -1372,6 +1596,7 @@ $wgAutoConfirmCount = 0;
* array( APCOND_ISIP, ip ), *OR*
* array( APCOND_IPINRANGE, range ), *OR*
* array( APCOND_AGE_FROM_EDIT, seconds since first edit ), *OR*
+ * array( APCOND_BLOCKED ), *OR*
* similar constructs defined by extensions.
*
* If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any
@@ -1412,14 +1637,6 @@ $wgAvailableRights = array();
*/
$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
#
@@ -1466,10 +1683,10 @@ $wgCacheEpoch = '20030516000000';
/**
* Bump this number when changing the global style sheets and JavaScript.
* It should be appended in the query string of static CSS and JS includes,
- * to ensure that client-side caches don't keep obsolete copies of global
+ * to ensure that client-side caches do not keep obsolete copies of global
* styles.
*/
-$wgStyleVersion = '207';
+$wgStyleVersion = '270';
# Server-side caching:
@@ -1482,7 +1699,7 @@ $wgStyleVersion = '207';
$wgUseFileCache = false;
/** Directory where the cached page will be saved */
-$wgFileCacheDirectory = false; ///< defaults to "{$wgUploadDirectory}/cache";
+$wgFileCacheDirectory = false; ///< defaults to "$wgCacheDirectory/html";
/**
* When using the file cache, we can store the cached HTML gzipped to save disk
@@ -1581,6 +1798,9 @@ $wgUseSquid = false;
/** If you run Squid3 with ESI support, enable this (default:false): */
$wgUseESI = false;
+/** Send X-Vary-Options header for better caching (requires patched Squid) */
+$wgUseXVO = false;
+
/** Internal server name as known to Squid, if different */
# $wgInternalServer = 'http://yourinternal.tld:8000';
$wgInternalServer = $wgServer;
@@ -1692,7 +1912,7 @@ $wgAllowExternalImagesFrom = '';
* Or false to disable it
*/
$wgEnableImageWhitelist = true;
-
+
/** Allows to move images and other media files */
$wgAllowImageMoving = true;
@@ -1738,6 +1958,26 @@ $wgSpecialPageCacheUpdates = array(
$wgUseTeX = false;
/** Location of the texvc binary */
$wgTexvc = './math/texvc';
+/**
+ * Texvc background color
+ * use LaTeX color format as used in \special function
+ * for transparent background use value 'Transparent' for alpha transparency or
+ * 'transparent' for binary transparency.
+ */
+$wgTexvcBackgroundColor = 'transparent';
+
+/**
+ * Normally when generating math images, we double-check that the
+ * directories we want to write to exist, and that files that have
+ * been generated still exist when we need to bring them up again.
+ *
+ * This lets us give useful error messages in case of permission
+ * problems, and automatically rebuild images that have been lost.
+ *
+ * On a big site with heavy NFS traffic this can be slow and flaky,
+ * so sometimes we want to short-circuit it by setting this to false.
+ */
+$wgMathCheckFiles = true;
#
# Profiling / debugging
@@ -1766,8 +2006,6 @@ $wgUDPProfilerPort = '3811';
$wgDebugProfiling = false;
/** Output debug message on every wfProfileIn/wfProfileOut */
$wgDebugFunctionEntry = 0;
-/** Lots of debugging output from SquidUpdate.php */
-$wgDebugSquid = false;
/*
* Destination for wfIncrStats() data...
@@ -1800,6 +2038,18 @@ $wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z}
: '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\n\r\/{}]'; // PHP 5.0 workaround
/**
+ * Set to true to have the search engine count total
+ * search matches to present in the Special:Search UI.
+ * Not supported by every search engine shipped with MW.
+ *
+ * This could however be slow on larger wikis, and is pretty flaky
+ * with the current title vs content split. Recommend avoiding until
+ * that's been worked out cleanly; but this may aid in testing the
+ * search UI and API to confirm that the result count works.
+ */
+$wgCountTotalSearchHits = false;
+
+/**
* Template for OpenSearch suggestions, defaults to API action=opensearch
*
* Sites with heavy load would tipically have these point to a custom
@@ -1813,10 +2063,23 @@ $wgOpenSearchTemplate = false;
/**
* Enable suggestions while typing in search boxes
* (results are passed around in OpenSearch format)
+ * Requires $wgEnableOpenSearchSuggest = true;
*/
$wgEnableMWSuggest = false;
/**
+ * Enable OpenSearch suggestions requested by MediaWiki. Set this to
+ * false if you've disabled MWSuggest or another suggestion script and
+ * want reduce load caused by cached scripts pulling suggestions.
+ */
+$wgEnableOpenSearchSuggest = true;
+
+/**
+ * Expiry time for search suggestion responses
+ */
+$wgSearchSuggestCacheExpiry = 1200;
+
+/**
* Template for internal MediaWiki suggestion engine, defaults to API action=opensearch
*
* Placeholders: {searchTerms}, {namespaces}, {dbname}
@@ -1850,6 +2113,11 @@ $wgShowEXIF = function_exists( 'exif_read_data' );
* uploads do work.
*/
$wgRemoteUploads = false;
+
+/**
+ * Disable links to talk pages of anonymous users (IPs) in listings on special
+ * pages like page history, Special:Recentchanges, etc.
+ */
$wgDisableAnonTalk = false;
/**
* Do DELETE/INSERT for link updates instead of incremental
@@ -1900,7 +2168,7 @@ $wgFileExtensions = array( 'png', 'gif', 'jpg', 'jpeg' );
/** Files with these extensions will never be allowed as uploads. */
$wgFileBlacklist = array(
# HTML may contain cookie-stealing JavaScript and web bugs
- 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht',
+ 'html', 'htm', 'js', 'jsb', 'mhtml', 'mht', 'xhtml', 'xht',
# PHP scripts may execute arbitrary code on the server
'php', 'phtml', 'php3', 'php4', 'php5', 'phps',
# Other types that may be interpreted by some servers
@@ -1951,37 +2219,50 @@ $wgNamespacesWithSubpages = array(
NS_USER_TALK => true,
NS_PROJECT_TALK => true,
NS_FILE_TALK => true,
+ NS_MEDIAWIKI => true,
NS_MEDIAWIKI_TALK => true,
NS_TEMPLATE_TALK => true,
NS_HELP_TALK => true,
NS_CATEGORY_TALK => true
);
+/**
+ * Which namespaces have special treatment where they should be preview-on-open
+ * Internaly only Category: pages apply, but using this extensions (e.g. Semantic MediaWiki)
+ * can specify namespaces of pages they have special treatment for
+ */
+$wgPreviewOnOpenNamespaces = array(
+ NS_CATEGORY => true
+);
+
$wgNamespacesToBeSearchedDefault = array(
NS_MAIN => true,
);
/**
- * Additional namespaces to those in $wgNamespacesToBeSearchedDefault that
- * will be added to default search for "project" page inclusive searches
- *
+ * Namespaces to be searched when user clicks the "Help" tab
+ * on Special:Search
+ *
* Same format as $wgNamespacesToBeSearchedDefault
- */
-$wgNamespacesToBeSearchedProject = array(
- NS_USER => true,
- NS_PROJECT => true,
+ */
+$wgNamespacesToBeSearchedHelp = array(
+ NS_PROJECT => true,
NS_HELP => true,
- NS_CATEGORY => true,
);
-$wgUseOldSearchUI = true; // temp testing variable
+/**
+ * If set to true the 'searcheverything' preference will be effective only for logged-in users.
+ * Useful for big wikis to maintain different search profiles for anonymous and logged-in users.
+ *
+ */
+$wgSearchEverythingOnlyLoggedIn = false;
/**
* Site notice shown at the top of each page
*
- * This message can contain wiki text, and can also be set through the
- * MediaWiki:Sitenotice page. You can also provide a separate message for
- * logged-out users using the MediaWiki:Anonnotice page.
+ * MediaWiki:Sitenotice page, which will override this. You can also
+ * provide a separate message for logged-out users using the
+ * MediaWiki:Anonnotice page.
*/
$wgSiteNotice = '';
@@ -1996,7 +2277,7 @@ $wgSiteNotice = '';
$wgMediaHandlers = array(
'image/jpeg' => 'BitmapHandler',
'image/png' => 'BitmapHandler',
- 'image/gif' => 'BitmapHandler',
+ 'image/gif' => 'GIFHandler',
'image/tiff' => 'TiffHandler',
'image/x-ms-bmp' => 'BmpHandler',
'image/x-bmp' => 'BmpHandler',
@@ -2026,8 +2307,8 @@ $wgSharpenParameter = '0x0.4';
/** Reduction in linear dimensions below which sharpening will be enabled */
$wgSharpenReductionThreshold = 0.85;
-/**
- * Temporary directory used for ImageMagick. The directory must exist. Leave
+/**
+ * Temporary directory used for ImageMagick. The directory must exist. Leave
* this set to false to let ImageMagick decide for itself.
*/
$wgImageMagickTempDir = false;
@@ -2084,7 +2365,8 @@ $wgMaxAnimatedGifArea = 1.0e6;
* // JPEG is good for photos, but has no transparency support. Bad for diagrams.
* $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' );
*/
-$wgTiffThumbnailType = false;
+ $wgTiffThumbnailType = false;
+
/**
* If rendered thumbnail files are older than this timestamp, they
* will be rerendered on demand as if the file didn't already exist.
@@ -2115,9 +2397,15 @@ $wgIgnoreImageErrors = false;
*/
$wgGenerateThumbnailOnParse = true;
-/** Whether or not to use image resizing */
+/**
+* Show thumbnails for old images on the image description page
+*/
+$wgShowArchiveThumbnails = true;
+
+/** Obsolete, always true, kept for compatibility with extensions */
$wgUseImageResize = true;
+
/** Set $wgCommandLineMode if it's not set already, to avoid notices */
if( !isset( $wgCommandLineMode ) ) {
$wgCommandLineMode = false;
@@ -2126,6 +2414,13 @@ if( !isset( $wgCommandLineMode ) ) {
/** For colorized maintenance script output, is your terminal background dark ? */
$wgCommandLineDarkBg = false;
+/**
+ * Array for extensions to register their maintenance scripts with the
+ * system. The key is the name of the class and the value is the full
+ * path to the file
+ */
+$wgMaintenanceScripts = array();
+
#
# Recent changes settings
#
@@ -2136,9 +2431,9 @@ $wgPutIPinRC = true;
/**
* Recentchanges items are periodically purged; entries older than this many
* seconds will go.
- * For one week : 7 * 24 * 3600
+ * Default: 13 weeks = about three months
*/
-$wgRCMaxAge = 7 * 24 * 3600;
+$wgRCMaxAge = 13 * 7 * 24 * 3600;
/**
* Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers higher than what will be stored.
@@ -2167,19 +2462,19 @@ $wgRC2UDPPort = false;
/**
* Prefix to prepend to each UDP packet.
* This can be used to identify the wiki. A script is available called
- * mxircecho.py which listens on a UDP port, and uses a prefix ending in a
+ * mxircecho.py which listens on a UDP port, and uses a prefix ending in a
* tab to identify the IRC channel to send the log line to.
*/
$wgRC2UDPPrefix = '';
/**
- * If this is set to true, $wgLocalInterwiki will be prepended to links in the
+ * If this is set to true, $wgLocalInterwiki will be prepended to links in the
* IRC feed. If this is set to a string, that string will be used as the prefix.
*/
$wgRC2UDPInterwikiPrefix = false;
/**
- * Set to true to omit "bot" edits (by users with the bot permission) from the
+ * Set to true to omit "bot" edits (by users with the bot permission) from the
* UDP feed.
*/
$wgRC2UDPOmitBots = false;
@@ -2191,16 +2486,6 @@ $wgRC2UDPOmitBots = false;
*/
$wgEnableNewpagesUserFilter = true;
-/**
- * Whether to use metadata edition
- * This will put categories, language links and allowed templates in a separate text box
- * while editing pages
- * EXPERIMENTAL
- */
-$wgUseMetadataEdit = false;
-/** Full name (including namespace) of the page containing templates names that will be allowed as metadata */
-$wgMetadataWhitelist = '';
-
#
# Copyright and credits settings
#
@@ -2212,13 +2497,13 @@ $wgEnableCreativeCommonsRdf = false;
/** Override for copyright metadata.
* TODO: these options need documentation
*/
-$wgRightsPage = NULL;
-$wgRightsUrl = NULL;
-$wgRightsText = NULL;
-$wgRightsIcon = NULL;
+$wgRightsPage = null;
+$wgRightsUrl = null;
+$wgRightsText = null;
+$wgRightsIcon = null;
/** Set this to some HTML to override the rights icon with an arbitrary logo */
-$wgCopyrightIcon = NULL;
+$wgCopyrightIcon = null;
/** Set this to true if you want detailed copyright information forms on Upload. */
$wgUseCopyrightUpload = false;
@@ -2251,6 +2536,18 @@ $wgShowCreditsIfMax = true;
$wgCapitalLinks = true;
/**
+ * @since 1.16 - This can now be set per-namespace. Some special namespaces (such
+ * as Special, see MWNamespace::$alwaysCapitalizedNamespaces for the full list) must be
+ * true by default (and setting them has no effect), due to various things that
+ * require them to be so. Also, since Talk namespaces need to directly mirror their
+ * associated content namespaces, the values for those are ignored in favor of the
+ * subject namespace's setting. Setting for NS_MEDIA is taken automatically from
+ * NS_FILE.
+ * EX: $wgCapitalLinkOverrides[ NS_FILE ] = false;
+ */
+$wgCapitalLinkOverrides = array();
+
+/**
* List of interwiki prefixes for wikis we'll accept as sources for
* Special:Import (for sysops). Since complete page history can be imported,
* these should be 'trusted'.
@@ -2283,6 +2580,9 @@ $wgExportAllowHistory = true;
*/
$wgExportMaxHistory = 0;
+/**
+* Return distinct author list (when not returning full history)
+*/
$wgExportAllowListContributors = false ;
/**
@@ -2299,8 +2599,8 @@ $wgExportAllowListContributors = false ;
$wgExportMaxLinkDepth = 0;
/**
- * Whether to allow the "export all pages in namespace" option
- */
+* Whether to allow the "export all pages in namespace" option
+*/
$wgExportFromNamespaces = false;
/**
@@ -2311,6 +2611,7 @@ $wgExportFromNamespaces = false;
* May be an array of regexes or a single string for backwards compatibility.
*
* See http://en.wikipedia.org/wiki/Regular_expression
+ * Note that each regex needs a beginning/end delimiter, eg: # or /
*/
$wgSpamRegex = array();
@@ -2375,7 +2676,10 @@ $wgValidateAllHtml = false;
/** See list of skins and their symbolic names in languages/Language.php */
$wgDefaultSkin = 'monobook';
-/** Should we allow the user's to select their own skin that will override the default? */
+/**
+* Should we allow the user's to select their own skin that will override the default?
+* @deprecated in 1.16, use $wgHiddenPrefs[] = 'skin' to disable it
+*/
$wgAllowUserSkin = true;
/**
@@ -2475,11 +2779,21 @@ $wgDefaultUserOptions = array(
'watchdeletion' => 0,
'noconvertlink' => 0,
'gender' => 'unknown',
+ 'ccmeonemails' => 0,
+ 'disablemail' => 0,
+ 'editfont' => 'default',
);
-/** Whether or not to allow and use real name fields. Defaults to true. */
+/**
+ * Whether or not to allow and use real name fields.
+ * @deprecated in 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real
+ * names
+ */
$wgAllowRealName = true;
+/** An array of preferences to not show for the user */
+$wgHiddenPrefs = array();
+
/*****************************************************************************
* Extensions
*/
@@ -2496,10 +2810,15 @@ $wgExtensionFunctions = array();
$wgSkinExtensionFunctions = array();
/**
- * Extension messages files
- * Associative array mapping extension name to the filename where messages can be found.
- * The file must create a variable called $messages.
- * When the messages are needed, the extension should call wfLoadExtensionMessages().
+ * Extension messages files.
+ *
+ * Associative array mapping extension name to the filename where messages can be
+ * found. The file should contain variable assignments. Any of the variables
+ * present in languages/messages/MessagesEn.php may be defined, but $messages
+ * is the most common.
+ *
+ * Variables defined in extensions will override conflicting variables defined
+ * in the core.
*
* Example:
* $wgExtensionMessagesFiles['ConfirmEdit'] = dirname(__FILE__).'/ConfirmEdit.i18n.php';
@@ -2509,13 +2828,7 @@ $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';
+ * @deprecated Use $specialPageAliases in a file referred to by $wgExtensionMessagesFiles
*/
$wgExtensionAliasesFiles = array();
@@ -2560,8 +2873,8 @@ $wgAutoloadClasses = array();
* <code>
* $wgExtensionCredits[$type][] = array(
* 'name' => 'Example extension',
- * 'version' => 1.9,
- * 'svn-revision' => '$LastChangedRevision: 70070 $',
+ * 'version' => 1.9,
+ * 'path' => __FILE__,
* 'author' => 'Foo Barstein',
* 'url' => 'http://wwww.example.com/Example%20Extension/',
* 'description' => 'An example extension',
@@ -2570,6 +2883,8 @@ $wgAutoloadClasses = array();
* </code>
*
* Where $type is 'specialpage', 'parserhook', 'variable', 'media' or 'other'.
+ * Where 'descriptionmsg' can be an array with message key and parameters:
+ * 'descriptionmsg' => array( 'exampleextension-desc', param1, param2, ... ),
*/
$wgExtensionCredits = array();
/*
@@ -2596,7 +2911,11 @@ $wgUseSiteJs = true;
/** Use the site's Cascading Style Sheets (CSS)? */
$wgUseSiteCss = true;
-/** Filter for Special:Randompage. Part of a WHERE clause */
+/**
+ * Filter for Special:Randompage. Part of a WHERE clause
+ * @deprecated as of 1.16, use the SpecialRandomGetRandomTitle hook
+*/
+
$wgExtraRandompageSQL = false;
/** Allow the "info" action, very inefficient at the moment */
@@ -2608,9 +2927,6 @@ $wgMaxTocLevel = 999;
/** Name of the external diff engine to use */
$wgExternalDiffEngine = false;
-/** Whether to use inline diff */
-$wgEnableHtmlDiff = false;
-
/** Use RC Patrolling to check for vandalism */
$wgUseRCPatrol = true;
@@ -2646,6 +2962,12 @@ $wgFeedDiffCutoff = 32768;
$wgOverrideSiteFeed = array();
/**
+ * Which feed types should we provide by default? This can include 'rss',
+ * 'atom', neither, or both.
+ */
+$wgAdvertisedFeedTypes = array( 'atom' );
+
+/**
* Additional namespaces. If the namespaces defined in Language.php and
* Namespace.php are insufficient, you can create new ones here, for example,
* to import Help files in other languages.
@@ -2662,7 +2984,7 @@ $wgOverrideSiteFeed = array();
# 102 => "Aide",
# 103 => "Discussion_Aide"
# );
-$wgExtraNamespaces = NULL;
+$wgExtraNamespaces = null;
/**
* Namespace aliases
@@ -2777,10 +3099,10 @@ $wgBrowserBlackList = array(
/**
* Fake out the timezone that the server thinks it's in. This will be used for
* date display and not for what's stored in the DB. Leave to null to retain
- * your server's OS-based timezone value. This is the same as the timezone.
+ * your server's OS-based timezone value.
*
- * This variable is currently used ONLY for signature formatting, not for
- * anything else.
+ * This variable is currently used only for signature formatting and for local
+ * time/date parser variables ({{LOCALTIME}} etc.)
*
* Timezones can be translated by editing MediaWiki messages of type
* timezone-nameinlowercase like timezone-utc.
@@ -2802,10 +3124,10 @@ $wgLocaltimezone = null;
* $wgLocalTZoffset = date("Z") / 60;
*
* If your server is not configured for the timezone you want, you can set
- * this in conjunction with the signature timezone and override the TZ
- * environment variable like so:
+ * this in conjunction with the signature timezone and override the PHP default
+ * timezone like so:
* $wgLocaltimezone="Europe/Berlin";
- * putenv("TZ=$wgLocaltimezone");
+ * date_default_timezone_set( $wgLocaltimezone );
* $wgLocalTZoffset = date("Z") / 60;
*
* Leave at NULL to show times in universal time (UTC/GMT).
@@ -2871,6 +3193,7 @@ $wgLogTypes = array( '',
* 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
+ * Format: logtype => permissiontype
*/
$wgLogRestrictions = array(
'suppress' => 'suppressionlog'
@@ -2881,7 +3204,7 @@ $wgLogRestrictions = array(
*
* This is associative array of log type => boolean "hide by default"
*
- * See $wgLogTypes for a list of available log types.
+ * See $wgLogTypes for a list of available log types.
*
* For example:
* $wgFilterLogTypes => array(
@@ -2890,7 +3213,7 @@ $wgLogRestrictions = array(
* );
*
* Will display show/hide links for the move and import logs. Move logs will be
- * hidden by default unless the link is clicked. Import logs will be shown by
+ * hidden by default unless the link is clicked. Import logs will be shown by
* default, and hidden when the link is clicked.
*
* A message of the form log-show-hide-<type> should be added, and will be used
@@ -3025,7 +3348,7 @@ $wgSpecialPageGroups = array(
'Newimages' => 'changes',
'Newpages' => 'changes',
'Log' => 'changes',
- 'Tags' => 'changes',
+ 'Tags' => 'changes',
'Upload' => 'media',
'Listfiles' => 'media',
@@ -3034,6 +3357,7 @@ $wgSpecialPageGroups = array(
'Filepath' => 'media',
'Listusers' => 'users',
+ 'Activeusers' => 'users',
'Listgrouprights' => 'users',
'Ipblocklist' => 'users',
'Contributions' => 'users',
@@ -3088,14 +3412,6 @@ $wgSpecialPageGroups = array(
);
/**
- * Experimental preview feature to fetch rendered text
- * over an XMLHttpRequest from JavaScript instead of
- * forcing a submit and reload of the whole page.
- * Leave disabled unless you're testing it.
- */
-$wgLivePreview = false;
-
-/**
* Disable the internal MySQL-based search, to allow it to be
* implemented by an extension instead.
*/
@@ -3180,7 +3496,7 @@ $wgNamespaceRobotPolicies = array();
* 'Main_Page' => 'noindex,follow',
* # "Project", not the actual project name!
* 'Project:X' => 'index,follow',
- * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false)!
+ * # Needs to be "Abc", not "abc" (unless $wgCapitalLinks is false for that namespace)!
* 'abc' => 'noindex,nofollow'
* );
*/
@@ -3199,11 +3515,11 @@ $wgExemptFromUserRobotsControl = null;
* Specifies the minimal length of a user password. If set to 0, empty pass-
* words are allowed.
*/
-$wgMinimalPasswordLength = 0;
+$wgMinimalPasswordLength = 1;
/**
* Activate external editor interface for files and pages
- * See http://meta.wikimedia.org/wiki/Help:External_editors
+ * See http://www.mediawiki.org/wiki/Manual:External_editors
*/
$wgUseExternalEditor = true;
@@ -3231,10 +3547,35 @@ $wgDisabledActions = array();
$wgDisableHardRedirects = false;
/**
- * Use http.dnsbl.sorbs.net to check for open proxies
+ * Set to false to disable application of access keys and tooltips,
+ * eg to avoid keyboard conflicts with system keys or as a low-level
+ * optimization.
+ */
+$wgEnableTooltipsAndAccesskeys = true;
+
+/**
+ * Whether to use DNS blacklists in $wgDnsBlacklistUrls to check for open proxies
+ * @since 1.16
+ */
+$wgEnableDnsBlacklist = false;
+
+/**
+ * @deprecated Use $wgEnableDnsBlacklist instead, only kept for backward
+ * compatibility
*/
$wgEnableSorbs = false;
-$wgSorbsUrl = 'http.dnsbl.sorbs.net.';
+
+/**
+ * List of DNS blacklists to use, if $wgEnableDnsBlacklist is true
+ * @since 1.16
+ */
+$wgDnsBlacklistUrls = array( 'http.dnsbl.sorbs.net.' );
+
+/**
+ * @deprecated Use $wgDnsBlacklistUrls instead, only kept for backward
+ * compatibility
+ */
+$wgSorbsUrl = array();
/**
* Proxy whitelist, list of addresses that are assumed to be non-proxy despite
@@ -3266,7 +3607,7 @@ $wgRateLimits = array(
'subnet' => null,
),
'mailpassword' => array(
- 'anon' => NULL,
+ 'anon' => null,
),
'emailuser' => array(
'user' => null,
@@ -3364,9 +3705,14 @@ $wgTrustedMediaFormats= array(
$wgAllowSpecialInclusion = true;
/**
- * Timeout for HTTP requests done via CURL
+ * Timeout for HTTP requests done internally
+ */
+$wgHTTPTimeout = 25;
+
+/**
+ * Timeout for Asynchronous (background) HTTP requests
*/
-$wgHTTPTimeout = 3;
+$wgAsyncHTTPTimeout = 25;
/**
* Proxy to use for CURL requests.
@@ -3409,7 +3755,7 @@ $wgUpdateRowsPerJob = 500;
/**
* Number of rows to update per query
*/
-$wgUpdateRowsPerQuery = 10;
+$wgUpdateRowsPerQuery = 100;
/**
* Enable AJAX framework
@@ -3435,7 +3781,7 @@ $wgAjaxWatch = true;
$wgAjaxUploadDestCheck = true;
/**
- * Enable previewing licences via AJAX
+ * Enable previewing licences via AJAX. Also requires $wgEnableAPI to be true.
*/
$wgAjaxLicensePreview = true;
@@ -3495,9 +3841,9 @@ $wgMaxShellFileSize = 102400;
$wgMaxShellTime = 180;
/**
-* Executable name of PHP cli client (php/php5)
-*/
-$wgPhpCli = 'php';
+ * Executable path of the PHP cli binary (php/php5). Should be set up on install.
+ */
+$wgPhpCli = '/usr/bin/php';
/**
* DJVU settings
@@ -3515,6 +3861,13 @@ $wgDjvuDump = null;
$wgDjvuRenderer = null;
/**
+ * Path of the djvutxt DJVU text extraction utility
+ * Enable this and $wgDjvuDump to enable text layer extraction from djvu files
+ */
+# $wgDjvuTxt = 'djvutxt';
+$wgDjvuTxt = null;
+
+/**
* Path of the djvutoxml executable
* This works like djvudump except much, much slower as of version 3.5.
*
@@ -3582,6 +3935,24 @@ $wgAPIMaxResultSize = 8388608;
$wgAPIMaxUncachedDiffs = 1;
/**
+ * Log file or URL (TCP or UDP) to log API requests to, or false to disable
+ * API request logging
+ */
+$wgAPIRequestLog = false;
+
+/**
+ * Cache the API help text for up to an hour. Disable this during API
+ * debugging and development
+ */
+$wgAPICacheHelp = true;
+
+/**
+ * Set the timeout for the API help text cache. Ignored if $wgAPICacheHelp
+ * is false.
+ */
+$wgAPICacheHelpTimeout = 60*60;
+
+/**
* Parser test suite files to be run by parserTests.php when no specific
* filename is passed to it.
*
@@ -3595,6 +3966,21 @@ $wgParserTestFiles = array(
);
/**
+ * If configured, specifies target CodeReview installation to send test
+ * result data from 'parserTests.php --upload'
+ *
+ * Something like this:
+ * $wgParserTestRemote = array(
+ * 'api-url' => 'http://www.mediawiki.org/w/api.php',
+ * 'repo' => 'MediaWiki',
+ * 'suite' => 'ParserTests',
+ * 'path' => '/trunk/phase3', // not used client-side; for reference
+ * 'secret' => 'qmoicj3mc4mcklmqw', // Shared secret used in HMAC validation
+ * );
+ */
+$wgParserTestRemote = false;
+
+/**
* Break out of framesets. This can be used to prevent external sites from
* framing your site with ads.
*/
@@ -3652,6 +4038,12 @@ $wgParserConf = array(
$wgLinkHolderBatchSize = 1000;
/**
+ * By default MediaWiki does not register links pointing to same server in externallinks dataset,
+ * use this value to override:
+ */
+$wgRegisterInternalExternals = false;
+
+/**
* Hooks that are used for outputting exceptions. Format is:
* $wgExceptionHooks[] = $funcname
* or:
@@ -3661,8 +4053,11 @@ $wgLinkHolderBatchSize = 1000;
$wgExceptionHooks = array();
/**
- * Page property link table invalidation lists. Should only be set by exten-
- * sions.
+ * Page property link table invalidation lists. When a page property
+ * changes, this may require other link tables to be updated (eg
+ * adding __HIDDENCAT__ means the hiddencat tracking category will
+ * have been added, so the categorylinks table needs to be rebuilt).
+ * This array can be added to by extensions.
*/
$wgPagePropLinkInvalidations = array(
'hiddencat' => 'categorylinks',
@@ -3687,7 +4082,7 @@ $wgMaximumMovedPages = 100;
/**
* Fix double redirects after a page move.
- * Tends to conflict with page move vandalism, use only on a private wiki.
+ * Tends to conflict with page move vandalism, use only on a private wiki.
*/
$wgFixDoubleRedirects = false;
@@ -3709,7 +4104,7 @@ $wgMaxRedirects = 1;
* other namespaces cannot be invalidated by this variable.
*/
$wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk' );
-
+
/**
* Array of namespaces to generate a sitemap for when the
* maintenance/generateSitemap.php script is run, or false if one is to be ge-
@@ -3744,11 +4139,15 @@ $wgEdititis = false;
$wgUniversalEditButton = true;
/**
- * Allow id's that don't conform to HTML4 backward compatibility requirements.
- * This is currently for testing; if all goes well, this option will be removed
- * and the functionality will be enabled universally.
+ * Should we allow a broader set of characters in id attributes, per HTML5? If
+ * not, use only HTML 4-compatible IDs. This option is for testing -- when the
+ * functionality is ready, it will be on by default with no option.
+ *
+ * Currently this appears to work fine in Chrome 4 and 5, Firefox 3.5 and 3.6, IE6
+ * and 8, and Opera 10.50, but it fails in Opera 10.10: Unicode IDs don't seem
+ * to work as anchors. So not quite ready for general use yet.
*/
-$wgEnforceHtmlIds = true;
+$wgExperimentalHtmlIds = false;
/**
* Search form behavior
@@ -3758,6 +4157,28 @@ $wgEnforceHtmlIds = true;
$wgUseTwoButtonsSearchForm = true;
/**
+ * Search form behavior for Vector skin only
+ * true = use an icon search button
+ * false = use Go & Search buttons
+ */
+$wgVectorUseSimpleSearch = false;
+
+/**
+ * Watch and unwatch as an icon rather than a link for Vector skin only
+ * true = use an icon watch/unwatch button
+ * false = use watch/unwatch text link
+ */
+$wgVectorUseIconWatch = false;
+
+/**
+ * Add extra stylesheets for Vector - This is only being used so that we can play around with different options while
+ * keeping our CSS code in the SVN and not having to change the main Vector styles. This will probably go away later on.
+ * null = add no extra styles
+ * array = list of style paths relative to skins/vector/
+ */
+$wgVectorExtraStyles = null;
+
+/**
* Preprocessor caching threshold
*/
$wgPreprocessorCacheThreshold = 1000;
@@ -3791,3 +4212,122 @@ $wgInvalidUsernameCharacters = '@';
* modify the user rights of those users via Special:UserRights
*/
$wgUserrightsInterwikiDelimiter = '@';
+
+/**
+ * Configuration for processing pool control, for use in high-traffic wikis.
+ * An implementation is provided in the PoolCounter extension.
+ *
+ * This configuration array maps pool types to an associative array. The only
+ * defined key in the associative array is "class", which gives the class name.
+ * The remaining elements are passed through to the class as constructor
+ * parameters. Example:
+ *
+ * $wgPoolCounterConf = array( 'Article::view' => array(
+ * 'class' => 'PoolCounter_Client',
+ * ... any extension-specific options...
+ * );
+ */
+$wgPoolCounterConf = null;
+
+/**
+ * Use some particular type of external authentication. The specific
+ * authentication module you use will normally require some extra settings to
+ * be specified.
+ *
+ * null indicates no external authentication is to be used. Otherwise,
+ * $wgExternalAuthType must be the name of a non-abstract class that extends
+ * ExternalUser.
+ *
+ * Core authentication modules can be found in includes/extauth/.
+ */
+$wgExternalAuthType = null;
+
+/**
+ * Configuration for the external authentication. This may include arbitrary
+ * keys that depend on the authentication mechanism. For instance,
+ * authentication against another web app might require that the database login
+ * info be provided. Check the file where your auth mechanism is defined for
+ * info on what to put here.
+ */
+$wgExternalAuthConfig = array();
+
+/**
+ * When should we automatically create local accounts when external accounts
+ * already exist, if using ExternalAuth? Can have three values: 'never',
+ * 'login', 'view'. 'view' requires the external database to support cookies,
+ * and implies 'login'.
+ *
+ * TODO: Implement 'view' (currently behaves like 'login').
+ */
+$wgAutocreatePolicy = 'login';
+
+/**
+ * Policies for how each preference is allowed to be changed, in the presence
+ * of external authentication. The keys are preference keys, e.g., 'password'
+ * or 'emailaddress' (see Preferences.php et al.). The value can be one of the
+ * following:
+ *
+ * - local: Allow changes to this pref through the wiki interface but only
+ * apply them locally (default).
+ * - semiglobal: Allow changes through the wiki interface and try to apply them
+ * to the foreign database, but continue on anyway if that fails.
+ * - global: Allow changes through the wiki interface, but only let them go
+ * through if they successfully update the foreign database.
+ * - message: Allow no local changes for linked accounts; replace the change
+ * form with a message provided by the auth plugin, telling the user how to
+ * change the setting externally (maybe providing a link, etc.). If the auth
+ * plugin provides no message for this preference, hide it entirely.
+ *
+ * Accounts that are not linked to an external account are never affected by
+ * this setting. You may want to look at $wgHiddenPrefs instead.
+ * $wgHiddenPrefs supersedes this option.
+ *
+ * TODO: Implement message, global.
+ */
+$wgAllowPrefChange = array();
+
+
+/**
+ * Settings for incoming cross-site AJAX requests:
+ * Newer browsers support cross-site AJAX when the target resource allows requests
+ * from the origin domain by the Access-Control-Allow-Origin header.
+ * This is currently only used by the API (requests to api.php)
+ * $wgCrossSiteAJAXdomains can be set using a wildcard syntax:
+ *
+ * '*' matches any number of characters
+ * '?' matches any 1 character
+ *
+ * Example:
+ $wgCrossSiteAJAXdomains = array(
+ 'www.mediawiki.org',
+ '*.wikipedia.org',
+ '*.wikimedia.org',
+ '*.wiktionary.org',
+ );
+ *
+ */
+$wgCrossSiteAJAXdomains = array();
+
+/**
+ * Domains that should not be allowed to make AJAX requests,
+ * even if they match one of the domains allowed by $wgCrossSiteAJAXdomains
+ * Uses the same syntax as $wgCrossSiteAJAXdomains
+ */
+
+$wgCrossSiteAJAXdomainExceptions = array();
+
+/**
+ * The minimum amount of memory that MediaWiki "needs"; MediaWiki will try to raise PHP's memory limit if it's below this amount.
+ */
+$wgMemoryLimit = "50M";
+
+/**
+ * To disable file delete/restore temporarily
+ */
+$wgUploadMaintenance = false;
+
+/**
+ * Use old names for change_tags indices.
+ */
+$wgOldChangeTagsIndex = false;
+
diff --git a/includes/Defines.php b/includes/Defines.php
index 8de6c5a1..7be569af 100644
--- a/includes/Defines.php
+++ b/includes/Defines.php
@@ -18,6 +18,7 @@ define( 'DBO_IGNORE', 4 );
define( 'DBO_TRX', 8 );
define( 'DBO_DEFAULT', 16 );
define( 'DBO_PERSISTENT', 32 );
+define( 'DBO_SYSDBA', 64 ); //for oracle maintenance
/**#@-*/
# Valid database indexes
@@ -102,7 +103,7 @@ define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works
define( 'CACHE_NONE', 0 ); // Do not cache
define( 'CACHE_DB', 1 ); // Store cache objects in the DB
define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCacheServers
-define( 'CACHE_ACCEL', 3 ); // eAccelerator or Turck, whichever is available
+define( 'CACHE_ACCEL', 3 ); // eAccelerator
define( 'CACHE_DBA', 4 ); // Use PHP's DBA extension to store in a DBM-style database
/**#@-*/
@@ -200,6 +201,7 @@ require_once dirname(__FILE__).'/normal/UtfNormalDefines.php';
# Hook support constants
define( 'MW_SUPPORTS_EDITFILTERMERGED', 1 );
define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 );
+define( 'MW_SUPPORTS_LOCALISATIONCACHE', 1 );
# Allowed values for Parser::$mOutputType
# Parameter to Parser::startExternalParse().
@@ -227,3 +229,4 @@ define( 'APCOND_INGROUPS', 4 );
define( 'APCOND_ISIP', 5 );
define( 'APCOND_IPINRANGE', 6 );
define( 'APCOND_AGE_FROM_EDIT', 7 );
+define( 'APCOND_BLOCKED', 8 );
diff --git a/includes/DjVuImage.php b/includes/DjVuImage.php
index 8e7caf63..75df0fd5 100644
--- a/includes/DjVuImage.php
+++ b/includes/DjVuImage.php
@@ -224,7 +224,7 @@ class DjVuImage {
* @return string
*/
function retrieveMetaData() {
- global $wgDjvuToXML, $wgDjvuDump;
+ global $wgDjvuToXML, $wgDjvuDump, $wgDjvuTxt;
if ( isset( $wgDjvuDump ) ) {
# djvudump is faster as of version 3.5
# http://sourceforge.net/tracker/index.php?func=detail&aid=1704049&group_id=32953&atid=406583
@@ -242,6 +242,30 @@ class DjVuImage {
} else {
$xml = null;
}
+ # Text layer
+ if ( isset( $wgDjvuTxt ) ) {
+ wfProfileIn( 'djvutxt' );
+ $cmd = wfEscapeShellArg( $wgDjvuTxt ) . ' --detail=page ' . wfEscapeShellArg( $this->mFilename ) ;
+ wfDebug( __METHOD__.": $cmd\n" );
+ $txt = wfShellExec( $cmd, $retval );
+ wfProfileOut( 'djvutxt' );
+ if( $retval == 0) {
+ # Get rid of invalid UTF-8, strip control characters
+ if( is_callable( 'iconv' ) ) {
+ wfSuppressWarnings();
+ $txt = iconv( "UTF-8","UTF-8//IGNORE", $txt );
+ wfRestoreWarnings();
+ } else {
+ $txt = UtfNormal::cleanUp( $txt );
+ }
+ $txt = preg_replace( "/[\013\035\037]/", "", $txt );
+ $txt = htmlspecialchars($txt);
+ $txt = preg_replace( "/\((page\s[\d-]*\s[\d-]*\s[\d-]*\s[\d-]*\s*\&quot;([^<]*?)\&quot;\s*|)\)/s", "<PAGE value=\"$2\" />", $txt );
+ $txt = "<DjVuTxt>\n<HEAD></HEAD>\n<BODY>\n" . $txt . "</BODY>\n</DjVuTxt>\n";
+ $xml = preg_replace( "/<DjVuXML>/", "<mw-djvu><DjVuXML>", $xml );
+ $xml = $xml . $txt. '</mw-djvu>' ;
+ }
+ }
return $xml;
}
diff --git a/includes/DoubleRedirectJob.php b/includes/DoubleRedirectJob.php
index 889beecf..0857408a 100644
--- a/includes/DoubleRedirectJob.php
+++ b/includes/DoubleRedirectJob.php
@@ -1,13 +1,19 @@
<?php
+/**
+ * Job to fix double redirects after moving a page
+ *
+ * @ingroup JobQueue
+ */
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
+ * @param $reason String: the reason for the fix, see message double-redirect-fixed-<reason>
+ * @param $redirTitle Title: the title which has changed, redirects pointing to this title are fixed
+ * @param $destTitle Not used
*/
public static function fixRedirects( $reason, $redirTitle, $destTitle = false ) {
# Need to use the master to get the redirect table updated in the same transaction
@@ -116,7 +122,7 @@ class DoubleRedirectJob extends Job {
/**
* Get the final destination of a redirect
- * Returns false if the specified title is not a redirect, or if it is a circular redirect
+ * @return 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 );
diff --git a/includes/EditPage.php b/includes/EditPage.php
index 3589b52d..b4cbf0de 100644
--- a/includes/EditPage.php
+++ b/includes/EditPage.php
@@ -17,37 +17,38 @@
* usually the same, but they are now allowed to be different.
*/
class EditPage {
- const AS_SUCCESS_UPDATE = 200;
- const AS_SUCCESS_NEW_ARTICLE = 201;
- const AS_HOOK_ERROR = 210;
- const AS_FILTERING = 211;
- const AS_HOOK_ERROR_EXPECTED = 212;
- const AS_BLOCKED_PAGE_FOR_USER = 215;
- const AS_CONTENT_TOO_BIG = 216;
- const AS_USER_CANNOT_EDIT = 217;
- const AS_READ_ONLY_PAGE_ANON = 218;
- const AS_READ_ONLY_PAGE_LOGGED = 219;
- const AS_READ_ONLY_PAGE = 220;
- const AS_RATE_LIMITED = 221;
- const AS_ARTICLE_WAS_DELETED = 222;
- const AS_NO_CREATE_PERMISSION = 223;
- const AS_BLANK_ARTICLE = 224;
- const AS_CONFLICT_DETECTED = 225;
- const AS_SUMMARY_NEEDED = 226;
- const AS_TEXTBOX_EMPTY = 228;
- const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
- const AS_OK = 230;
- const AS_END = 231;
- const AS_SPAM_ERROR = 232;
- const AS_IMAGE_REDIRECT_ANON = 233;
- const AS_IMAGE_REDIRECT_LOGGED = 234;
+ const AS_SUCCESS_UPDATE = 200;
+ const AS_SUCCESS_NEW_ARTICLE = 201;
+ const AS_HOOK_ERROR = 210;
+ const AS_FILTERING = 211;
+ const AS_HOOK_ERROR_EXPECTED = 212;
+ const AS_BLOCKED_PAGE_FOR_USER = 215;
+ const AS_CONTENT_TOO_BIG = 216;
+ const AS_USER_CANNOT_EDIT = 217;
+ const AS_READ_ONLY_PAGE_ANON = 218;
+ const AS_READ_ONLY_PAGE_LOGGED = 219;
+ const AS_READ_ONLY_PAGE = 220;
+ const AS_RATE_LIMITED = 221;
+ const AS_ARTICLE_WAS_DELETED = 222;
+ const AS_NO_CREATE_PERMISSION = 223;
+ const AS_BLANK_ARTICLE = 224;
+ const AS_CONFLICT_DETECTED = 225;
+ const AS_SUMMARY_NEEDED = 226;
+ const AS_TEXTBOX_EMPTY = 228;
+ const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
+ const AS_OK = 230;
+ const AS_END = 231;
+ const AS_SPAM_ERROR = 232;
+ const AS_IMAGE_REDIRECT_ANON = 233;
+ const AS_IMAGE_REDIRECT_LOGGED = 234;
var $mArticle;
var $mTitle;
var $action;
- var $mMetaData = '';
var $isConflict = false;
var $isCssJsSubpage = false;
+ var $isCssSubpage = false;
+ var $isJsSubpage = false;
var $deletedSinceEdit = false;
var $formtype;
var $firsttime;
@@ -65,13 +66,14 @@ class EditPage {
#var $mPreviewTemplates;
var $mParserOutput;
var $mBaseRevision = false;
+ var $mShowSummaryField = true;
# Form values
var $save = false, $preview = false, $diff = false;
var $minoredit = false, $watchthis = false, $recreate = false;
- var $textbox1 = '', $textbox2 = '', $summary = '';
+ var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
var $edittime = '', $section = '', $starttime = '';
- var $oldid = 0, $editintro = '', $scrolltop = null;
+ var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
# Placeholders for text injection by hooks (must be HTML)
# extensions should take care to _append_ to the present value
@@ -81,6 +83,8 @@ class EditPage {
public $editFormTextAfterWarn;
public $editFormTextAfterTools;
public $editFormTextBottom;
+ public $editFormTextAfterContent;
+ public $previewTextAfterContent;
/* $didSave should be set to true whenever an article was succesfully altered. */
public $didSave = false;
@@ -103,15 +107,20 @@ class EditPage {
$this->editFormTextBeforeContent =
$this->editFormTextAfterWarn =
$this->editFormTextAfterTools =
- $this->editFormTextBottom = "";
+ $this->editFormTextBottom =
+ $this->editFormTextAfterContent =
+ $this->previewTextAfterContent =
+ $this->mPreloadText = "";
}
-
+
function getArticle() {
return $this->mArticle;
}
+
/**
* Fetch initial editing page content.
+ * @returns mixed string on success, $def_text for invalid sections
* @private
*/
function getContent( $def_text = '' ) {
@@ -120,7 +129,10 @@ class EditPage {
wfProfileIn( __METHOD__ );
# Get variables from query string :P
$section = $wgRequest->getVal( 'section' );
- $preload = $wgRequest->getVal( 'preload' );
+
+ $preload = $wgRequest->getVal( 'preload',
+ // Custom preload text for new sections
+ $section === 'new' ? 'MediaWiki:addsection-preload' : '' );
$undoafter = $wgRequest->getVal( 'undoafter' );
$undo = $wgRequest->getVal( 'undo' );
@@ -134,7 +146,7 @@ class EditPage {
$wgMessageCache->loadAllMessages( $lang );
$text = wfMsgGetKey( $message, false, $lang, false );
if( wfEmptyMsg( $message, $text ) )
- $text = '';
+ $text = $this->getPreloadedText( $preload );
} else {
# If requested, preload some text.
$text = $this->getPreloadedText( $preload );
@@ -150,10 +162,10 @@ class EditPage {
# Undoing a specific edit overrides section editing; section-editing
# doesn't work with undoing.
if ( $undoafter ) {
- $undorev = Revision::newFromId($undo);
- $oldrev = Revision::newFromId($undoafter);
+ $undorev = Revision::newFromId( $undo );
+ $oldrev = Revision::newFromId( $undoafter );
} else {
- $undorev = Revision::newFromId($undo);
+ $undorev = Revision::newFromId( $undo );
$oldrev = $undorev ? $undorev->getPrevious() : null;
}
@@ -165,7 +177,7 @@ class EditPage {
$undorev->getPage() == $this->mArticle->getID() &&
!$undorev->isDeleted( Revision::DELETED_TEXT ) &&
!$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
-
+
$undotext = $this->mArticle->getUndoText( $undorev, $oldrev );
if ( $undotext === false ) {
# Warn the user that something went wrong
@@ -192,6 +204,7 @@ class EditPage {
if ( $section == 'new' ) {
$text = $this->getPreloadedText( $preload );
} else {
+ // Get section edit text (returns $def_text for invalid sections)
$text = $wgParser->getSection( $text, $section, $def_text );
}
}
@@ -201,6 +214,11 @@ class EditPage {
return $text;
}
+ /** Use this method before edit() to preload some text into the edit box */
+ public function setPreloadedText( $text ) {
+ $this->mPreloadText = $text;
+ }
+
/**
* Get the contents of a page from its title and remove includeonly tags
*
@@ -208,12 +226,14 @@ class EditPage {
* @return string The contents of the page.
*/
protected function getPreloadedText( $preload ) {
- if ( $preload === '' ) {
+ if ( !empty( $this->mPreloadText ) ) {
+ return $this->mPreloadText;
+ } elseif ( $preload === '' ) {
return '';
} else {
$preloadTitle = Title::newFromText( $preload );
if ( isset( $preloadTitle ) && $preloadTitle->userCanRead() ) {
- $rev = Revision::newFromTitle($preloadTitle);
+ $rev = Revision::newFromTitle( $preloadTitle );
if ( is_object( $rev ) ) {
$text = $rev->getText();
// TODO FIXME: AAAAAAAAAAA, this shouldn't be implementing
@@ -226,96 +246,7 @@ class EditPage {
}
}
- /**
- * This is the function that extracts metadata from the article body on the first view.
- * To turn the feature on, set $wgUseMetadataEdit = true ; in LocalSettings
- * and set $wgMetadataWhitelist to the *full* title of the template whitelist
- */
- function extractMetaDataFromArticle () {
- global $wgUseMetadataEdit, $wgMetadataWhitelist, $wgContLang;
- $this->mMetaData = '';
- if ( !$wgUseMetadataEdit ) return;
- if ( $wgMetadataWhitelist == '' ) return;
- $s = '';
- $t = $this->getContent();
-
- # MISSING : <nowiki> filtering
-
- # Categories and language links
- $t = explode ( "\n" , $t );
- $catlow = strtolower ( $wgContLang->getNsText( NS_CATEGORY ) );
- $cat = $ll = array();
- foreach ( $t AS $key => $x ) {
- $y = trim ( strtolower ( $x ) );
- while ( substr ( $y , 0 , 2 ) == '[[' ) {
- $y = explode ( ']]' , trim ( $x ) );
- $first = array_shift ( $y );
- $first = explode ( ':' , $first );
- $ns = array_shift ( $first );
- $ns = trim ( str_replace ( '[' , '' , $ns ) );
- if ( $wgContLang->getLanguageName( $ns ) || strtolower ( $ns ) == $catlow ) {
- $add = '[[' . $ns . ':' . implode ( ':' , $first ) . ']]';
- if ( strtolower ( $ns ) == $catlow ) $cat[] = $add;
- else $ll[] = $add;
- $x = implode ( ']]' , $y );
- $t[$key] = $x;
- $y = trim ( strtolower ( $x ) );
- } else {
- $x = implode ( ']]' , $y );
- $y = trim ( strtolower ( $x ) );
- }
- }
- }
- if ( count ( $cat ) ) $s .= implode ( ' ' , $cat ) . "\n";
- if ( count ( $ll ) ) $s .= implode ( ' ' , $ll ) . "\n";
- $t = implode ( "\n" , $t );
-
- # Load whitelist
- $sat = array () ; # stand-alone-templates; must be lowercase
- $wl_title = Title::newFromText ( $wgMetadataWhitelist );
- $wl_article = new Article ( $wl_title );
- $wl = explode ( "\n" , $wl_article->getContent() );
- foreach ( $wl AS $x ) {
- $isentry = false;
- $x = trim ( $x );
- while ( substr ( $x , 0 , 1 ) == '*' ) {
- $isentry = true;
- $x = trim ( substr ( $x , 1 ) );
- }
- if ( $isentry ) {
- $sat[] = strtolower ( $x );
- }
-
- }
-
- # Templates, but only some
- $t = explode ( '{{' , $t );
- $tl = array () ;
- foreach ( $t AS $key => $x ) {
- $y = explode ( '}}' , $x , 2 );
- if ( count ( $y ) == 2 ) {
- $z = $y[0];
- $z = explode ( '|' , $z );
- $tn = array_shift ( $z );
- if ( in_array ( strtolower ( $tn ) , $sat ) ) {
- $tl[] = '{{' . $y[0] . '}}';
- $t[$key] = $y[1];
- $y = explode ( '}}' , $y[1] , 2 );
- }
- else $t[$key] = '{{' . $x;
- }
- else if ( $key != 0 ) $t[$key] = '{{' . $x;
- else $t[$key] = $x;
- }
- if ( count ( $tl ) ) $s .= implode ( ' ' , $tl );
- $t = implode ( '' , $t );
-
- $t = str_replace ( "\n\n\n" , "\n" , $t );
- $this->mArticle->mContent = $t;
- $this->mMetaData = $s;
- }
-
- /*
+ /*
* Check if a page was deleted while the user was editing it, before submit.
* Note that we rely on the logging table, which hasn't been always there,
* but that doesn't matter, because this only applies to brand new
@@ -352,9 +283,9 @@ class EditPage {
* the newly-edited page.
*/
function edit() {
- global $wgOut, $wgUser, $wgRequest;
+ global $wgOut, $wgRequest, $wgUser;
// Allow extensions to modify/prevent this form or submission
- if ( !wfRunHooks( 'AlternateEdit', array( &$this ) ) ) {
+ if ( !wfRunHooks( 'AlternateEdit', array( $this ) ) ) {
return;
}
@@ -374,16 +305,24 @@ class EditPage {
}
if ( wfReadOnly() && $this->save ) {
- // Force preview
- $this->save = false;
- $this->preview = true;
+ // Force preview
+ $this->save = false;
+ $this->preview = true;
}
$wgOut->addScriptFile( 'edit.js' );
+
+ if ( $wgUser->getOption( 'uselivepreview', false ) ) {
+ $wgOut->includeJQuery();
+ $wgOut->addScriptFile( 'preview.js' );
+ }
+ // Bug #19334: textarea jumps when editing articles in IE8
+ $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' );
+
$permErrors = $this->getEditPermissionErrors();
if ( $permErrors ) {
- wfDebug( __METHOD__.": User can't edit\n" );
- $this->readOnlyPage( $this->getContent(), true, $permErrors, 'edit' );
+ wfDebug( __METHOD__ . ": User can't edit\n" );
+ $this->readOnlyPage( $this->getContent( false ), true, $permErrors, 'edit' );
wfProfileOut( __METHOD__ );
return;
} else {
@@ -398,12 +337,11 @@ class EditPage {
if ( $this->previewOnOpen() ) {
$this->formtype = 'preview';
} else {
- $this->extractMetaDataFromArticle () ;
$this->formtype = 'initial';
}
}
}
-
+
// If they used redlink=1 and the page exists, redirect to the main article
if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
$wgOut->redirect( $this->mTitle->getFullURL() );
@@ -414,6 +352,8 @@ class EditPage {
$this->isConflict = false;
// css / js subpages of user pages get a special treatment
$this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
+ $this->isCssSubpage = $this->mTitle->isCssSubpage();
+ $this->isJsSubpage = $this->mTitle->isJsSubpage();
$this->isValidCssJsSubpage = $this->mTitle->isValidCssJsSubpage();
# Show applicable editing introductions
@@ -456,7 +396,7 @@ class EditPage {
# First time through: get contents, set time for conflict
# checking, etc.
if ( 'initial' == $this->formtype || $this->firsttime ) {
- if ( $this->initialiseForm() === false) {
+ if ( $this->initialiseForm() === false ) {
$this->noSuchSectionPage();
wfProfileOut( __METHOD__."-business-end" );
wfProfileOut( __METHOD__ );
@@ -464,13 +404,15 @@ class EditPage {
}
if ( !$this->mTitle->getArticleId() )
wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
+ else
+ wfRunHooks( 'EditFormInitialText', array( $this ) );
}
$this->showEditForm();
wfProfileOut( __METHOD__."-business-end" );
wfProfileOut( __METHOD__ );
}
-
+
protected function getEditPermissionErrors() {
global $wgUser;
$permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
@@ -482,8 +424,8 @@ class EditPage {
# Ignore some permissions errors when a user is just previewing/viewing diffs
$remove = array();
foreach( $permErrors as $error ) {
- if ( ($this->preview || $this->diff) &&
- ($error[0] == 'blockedtext' || $error[0] == 'autoblockedtext') )
+ if ( ( $this->preview || $this->diff ) &&
+ ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' ) )
{
$remove[] = $error;
}
@@ -515,7 +457,7 @@ class EditPage {
* @return bool
*/
protected function previewOnOpen() {
- global $wgRequest, $wgUser;
+ global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
// Explicit override from request
return true;
@@ -528,7 +470,10 @@ class EditPage {
} elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) {
// Standard preference behaviour
return true;
- } elseif ( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) {
+ } elseif ( !$this->mTitle->exists() &&
+ isset($wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]) &&
+ $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
+ {
// Categories are special
return true;
} else {
@@ -537,13 +482,36 @@ class EditPage {
}
/**
+ * Does this EditPage class support section editing?
+ * This is used by EditPage subclasses to indicate their ui cannot handle section edits
+ *
+ * @return bool
+ */
+ protected function isSectionEditSupported() {
+ return true;
+ }
+
+ /**
+ * Returns the URL to use in the form's action attribute.
+ * This is used by EditPage subclasses when simply customizing the action
+ * variable in the constructor is not enough. This can be used when the
+ * EditPage lives inside of a Special page rather than a custom page action.
+ *
+ * @param Title $title The title for which is being edited (where we go to for &action= links)
+ * @return string
+ */
+ protected function getActionURL( Title $title ) {
+ return $title->getLocalURL( array( 'action' => $this->action ) );
+ }
+
+ /**
* @todo document
* @param $request
*/
function importFormData( &$request ) {
global $wgLang, $wgUser;
- $fname = 'EditPage::importFormData';
- wfProfileIn( $fname );
+
+ wfProfileIn( __METHOD__ );
# Section edit can come from either the form or a link
$this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
@@ -553,8 +521,17 @@ class EditPage {
# 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' ) );
+ if ( !$request->getCheck('wpTextbox2') ) {
+ // Skip this if wpTextbox2 has input, it indicates that we came
+ // from a conflict page with raw page text, not a custom form
+ // modified by subclasses
+ wfProfileIn( get_class($this)."::importContentFormData" );
+ $textbox1 = $this->importContentFormData( $request );
+ if ( isset($textbox1) )
+ $this->textbox1 = $textbox1;
+ wfProfileOut( get_class($this)."::importContentFormData" );
+ }
+
# Truncate for whole multibyte characters. +5 bytes for ellipsis
$this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' );
@@ -568,7 +545,7 @@ class EditPage {
if ( is_null( $this->edittime ) ) {
# If the form is incomplete, force to preview.
- wfDebug( "$fname: Form data appears to be incomplete\n" );
+ wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
$this->preview = true;
} else {
@@ -585,23 +562,23 @@ class EditPage {
# if the user hits enter in the comment box.
# The unmarked state will be assumed to be a save,
# if the form seems otherwise complete.
- wfDebug( "$fname: Passed token check.\n" );
+ wfDebug( __METHOD__ . ": Passed token check.\n" );
} else if ( $this->diff ) {
# Failed token check, but only requested "Show Changes".
- wfDebug( "$fname: Failed token check; Show Changes requested.\n" );
+ wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
} else {
# Page might be a hack attempt posted from
# an external site. Preview instead of saving.
- wfDebug( "$fname: Failed token check; forcing preview\n" );
+ wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
$this->preview = true;
}
}
$this->save = !$this->preview && !$this->diff;
- if ( !preg_match( '/^\d{14}$/', $this->edittime )) {
+ if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
$this->edittime = null;
}
- if ( !preg_match( '/^\d{14}$/', $this->starttime )) {
+ if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
$this->starttime = null;
}
@@ -611,8 +588,8 @@ class EditPage {
$this->watchthis = $request->getCheck( 'wpWatchthis' );
# Don't force edit summaries when a user is editing their own user or talk page
- if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) &&
- $this->mTitle->getText() == $wgUser->getName() )
+ if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK ) &&
+ $this->mTitle->getText() == $wgUser->getName() )
{
$this->allowBlankSummary = true;
} else {
@@ -622,10 +599,8 @@ class EditPage {
$this->autoSumm = $request->getText( 'wpAutoSummary' );
} else {
# Not a posted form? Start with nothing.
- wfDebug( "$fname: Not a posted form.\n" );
+ wfDebug( __METHOD__ . ": Not a posted form.\n" );
$this->textbox1 = '';
- $this->textbox2 = '';
- $this->mMetaData = '';
$this->summary = '';
$this->edittime = '';
$this->starttime = wfTimestampNow();
@@ -634,7 +609,7 @@ class EditPage {
$this->save = false;
$this->diff = false;
$this->minoredit = false;
- $this->watchthis = false;
+ $this->watchthis = $request->getBool( 'watchthis', false ); // Watch may be overriden by request parameters
$this->recreate = false;
if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
@@ -643,18 +618,39 @@ class EditPage {
elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
$this->summary = $request->getText( 'summary' );
}
-
+
if ( $request->getVal( 'minor' ) ) {
$this->minoredit = true;
}
}
+ $this->bot = $request->getBool( 'bot', true );
+ $this->nosummary = $request->getBool( 'nosummary' );
+
+ // FIXME: unused variable?
$this->oldid = $request->getInt( 'oldid' );
$this->live = $request->getCheck( 'live' );
- $this->editintro = $request->getText( 'editintro' );
+ $this->editintro = $request->getText( 'editintro',
+ // Custom edit intro for new sections
+ $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
+
+ wfProfileOut( __METHOD__ );
- wfProfileOut( $fname );
+ // Allow extensions to modify form data
+ wfRunHooks( 'EditPage::importFormData', array( $this, $request ) );
+ }
+
+ /**
+ * Subpage overridable method for extracting the page content data from the
+ * posted form to be placed in $this->textbox1, if using customized input
+ * this method should be overrided and return the page text that will be used
+ * for saving, preview parsing and so on...
+ *
+ * @praram WebRequest $request
+ */
+ protected function importContentFormData( &$request ) {
+ return; // Don't do anything, EditPage already extracted wpTextbox1
}
/**
@@ -688,28 +684,49 @@ class EditPage {
$wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1</div>", 'editinginterface' );
}
- # Show a warning message when someone creates/edits a user (talk) page but the user does not exists
+ # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
+ # Show log extract when the user is currently blocked
if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
$parts = explode( '/', $this->mTitle->getText(), 2 );
$username = $parts[0];
- $id = User::idFromName( $username );
+ $user = User::newFromName( $username, false /* allow IP users*/ );
$ip = User::isIP( $username );
- if ( $id == 0 && !$ip ) {
- $wgOut->wrapWikiMsg( '<div class="mw-userpage-userdoesnotexist error">$1</div>',
+ if ( !$user->isLoggedIn() && !$ip ) { # User does not exist
+ $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1</div>",
array( 'userpage-userdoesnotexist', $username ) );
+ } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'block',
+ $user->getUserPage()->getPrefixedText(),
+ '',
+ array(
+ 'lim' => 1,
+ 'showIfEmpty' => false,
+ 'msgKey' => array(
+ 'blocked-notice-logextract',
+ $user->getName() # Support GENDER in notice
+ )
+ )
+ );
}
}
# Try to add a custom edit intro, or use the standard one if this is not possible.
if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
if ( $wgUser->isLoggedIn() ) {
- $wgOut->wrapWikiMsg( '<div class="mw-newarticletext">$1</div>', 'newarticletext' );
+ $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletext\">\n$1</div>", 'newarticletext' );
} else {
- $wgOut->wrapWikiMsg( '<div class="mw-newarticletextanon">$1</div>', 'newarticletextanon' );
+ $wgOut->wrapWikiMsg( "<div class=\"mw-newarticletextanon\">\n$1</div>", 'newarticletextanon' );
}
}
- # Give a notice if the user is editing a deleted page...
+ # Give a notice if the user is editing a deleted/moved page...
if ( !$this->mTitle->exists() ) {
- $this->showDeletionLog( $wgOut );
+ LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(),
+ '', array( 'lim' => 10,
+ 'conds' => array( "log_action != 'revision'" ),
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'recreate-moveddeleted-warn') )
+ );
}
}
@@ -742,12 +759,10 @@ class EditPage {
global $wgFilterCallback, $wgUser, $wgOut, $wgParser;
global $wgMaxArticleSize;
- $fname = 'EditPage::attemptSave';
- wfProfileIn( $fname );
- wfProfileIn( "$fname-checks" );
+ wfProfileIn( __METHOD__ );
+ wfProfileIn( __METHOD__ . '-checks' );
- if ( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) )
- {
+ if ( !wfRunHooks( 'EditPage::attemptSave', array( $this ) ) ) {
wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
return self::AS_HOOK_ERROR;
}
@@ -763,10 +778,6 @@ class EditPage {
}
}
- # Reintegrate metadata
- if ( $this->mMetaData != '' ) $this->textbox1 .= "\n" . $this->mMetaData ;
- $this->mMetaData = '' ;
-
# Check for spam
$match = self::matchSummarySpamRegex( $this->summary );
if ( $match === false ) {
@@ -778,104 +789,107 @@ class EditPage {
$pdbk = $this->mTitle->getPrefixedDBkey();
$match = str_replace( "\n", '', $match );
wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_SPAM_ERROR;
}
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 );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_FILTERING;
}
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 );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_HOOK_ERROR;
} elseif ( $this->hookError != '' ) {
# ...or the hook could be expecting us to produce an error
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_HOOK_ERROR_EXPECTED;
}
if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
# Check block state against master, thus 'false'.
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_BLOCKED_PAGE_FOR_USER;
}
- $this->kblength = (int)(strlen( $this->textbox1 ) / 1024);
+ $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
if ( $this->kblength > $wgMaxArticleSize ) {
// Error will be displayed by showEditForm()
$this->tooBig = true;
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_CONTENT_TOO_BIG;
}
- if ( !$wgUser->isAllowed('edit') ) {
+ if ( !$wgUser->isAllowed( 'edit' ) ) {
if ( $wgUser->isAnon() ) {
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_READ_ONLY_PAGE_ANON;
- }
- else {
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ } else {
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_READ_ONLY_PAGE_LOGGED;
}
}
if ( wfReadOnly() ) {
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_READ_ONLY_PAGE;
}
if ( $wgUser->pingLimiter() ) {
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_RATE_LIMITED;
}
# If the article has been deleted while editing, don't save it without
# confirmation
if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
- wfProfileOut( "$fname-checks" );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ . '-checks' );
+ wfProfileOut( __METHOD__ );
return self::AS_ARTICLE_WAS_DELETED;
}
- wfProfileOut( "$fname-checks" );
+ wfProfileOut( __METHOD__ . '-checks' );
# If article is new, insert it.
$aid = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
if ( 0 == $aid ) {
// Late check for create permission, just in case *PARANOIA*
if ( !$this->mTitle->userCan( 'create' ) ) {
- wfDebug( "$fname: no create permission\n" );
- wfProfileOut( $fname );
+ wfDebug( __METHOD__ . ": no create permission\n" );
+ wfProfileOut( __METHOD__ );
return self::AS_NO_CREATE_PERMISSION;
}
# Don't save a new article if it's blank.
- if ( '' == $this->textbox1 ) {
- wfProfileOut( $fname );
+ if ( $this->textbox1 == '' ) {
+ wfProfileOut( __METHOD__ );
return self::AS_BLANK_ARTICLE;
}
// Run post-section-merge edit filter
if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
# Error messages etc. could be handled within the hook...
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_HOOK_ERROR;
+ } elseif ( $this->hookError != '' ) {
+ # ...or the hook could be expecting us to produce an error
+ wfProfileOut( __METHOD__ );
+ return self::AS_HOOK_ERROR_EXPECTED;
}
-
+
# Handle the user preference to force summaries here. Check if it's not a redirect.
if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) {
if ( md5( $this->summary ) == $this->autoSumm ) {
$this->missingSummary = true;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_SUMMARY_NEEDED;
}
}
@@ -885,7 +899,7 @@ class EditPage {
$this->mArticle->insertNewArticle( $this->textbox1, $this->summary,
$this->minoredit, $this->watchthis, false, $isComment, $bot );
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_SUCCESS_NEW_ARTICLE;
}
@@ -894,7 +908,7 @@ class EditPage {
$this->mArticle->clear(); # Force reload of dates, etc.
$this->mArticle->forUpdate( true ); # Lock the article
- wfDebug("timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n");
+ wfDebug( "timestamp: {$this->mArticle->getTimestamp()}, edittime: {$this->edittime}\n" );
if ( $this->mArticle->getTimestamp() != $this->edittime ) {
$this->isConflict = true;
@@ -904,32 +918,32 @@ class EditPage {
// Probably a duplicate submission of a new comment.
// This can happen when squid resends a request after
// a timeout but the first one actually went through.
- wfDebug( "EditPage::editForm duplicate new section submission; trigger edit conflict!\n" );
+ wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
} else {
// New comment; suppress conflict.
$this->isConflict = false;
- wfDebug( "EditPage::editForm conflict suppressed; new section\n" );
+ wfDebug( __METHOD__ .": conflict suppressed; new section\n" );
}
}
}
$userid = $wgUser->getId();
-
+
# Suppress edit conflict with self, except for section edits where merging is required.
- if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit($userid,$this->edittime) ) {
- wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" );
+ if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit( $userid, $this->edittime ) ) {
+ wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
$this->isConflict = false;
}
if ( $this->isConflict ) {
- wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" .
+ wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '" .
$this->mArticle->getTimestamp() . "')\n" );
$text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime );
} else {
- wfDebug( "EditPage::editForm getting section '$this->section'\n" );
+ wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
$text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary );
}
if ( is_null( $text ) ) {
- wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" );
+ wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
$this->isConflict = true;
$text = $this->textbox1; // do not try to merge here!
} else if ( $this->isConflict ) {
@@ -937,16 +951,16 @@ class EditPage {
if ( $this->mergeChangesInto( $text ) ) {
// Successful merge! Maybe we should tell the user the good news?
$this->isConflict = false;
- wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" );
+ wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
} else {
$this->section = '';
$this->textbox1 = $text;
- wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" );
+ wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
}
}
if ( $this->isConflict ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_CONFLICT_DETECTED;
}
@@ -955,36 +969,42 @@ class EditPage {
// Run post-section-merge edit filter
if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
# Error messages etc. could be handled within the hook...
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_HOOK_ERROR;
+ } elseif ( $this->hookError != '' ) {
+ # ...or the hook could be expecting us to produce an error
+ wfProfileOut( __METHOD__ );
+ return self::AS_HOOK_ERROR_EXPECTED;
}
# Handle the user preference to force summaries here, but not for null edits
- if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext,$text)
+ if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp( $oldtext, $text )
&& !Title::newFromRedirect( $text ) ) # check if it's not a redirect
{
if ( md5( $this->summary ) == $this->autoSumm ) {
$this->missingSummary = true;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_SUMMARY_NEEDED;
}
}
# And a similar thing for new sections
if ( $this->section == 'new' && !$this->allowBlankSummary ) {
- if (trim($this->summary) == '') {
+ if ( trim( $this->summary ) == '' ) {
$this->missingSummary = true;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_SUMMARY_NEEDED;
}
}
# All's well
- wfProfileIn( "$fname-sectionanchor" );
+ wfProfileIn( __METHOD__ . '-sectionanchor' );
$sectionanchor = '';
if ( $this->section == 'new' ) {
if ( $this->textbox1 == '' ) {
$this->missingComment = true;
+ wfProfileOut( __METHOD__ . '-sectionanchor' );
+ wfProfileOut( __METHOD__ );
return self::AS_TEXTBOX_EMPTY;
}
if ( $this->summary != '' ) {
@@ -1001,11 +1021,11 @@ class EditPage {
$hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
# we can't deal with anchors, includes, html etc in the header for now,
# headline would need to be parsed to improve this
- if ( $hasmatch and strlen($matches[2]) > 0 ) {
+ if ( $hasmatch and strlen( $matches[2] ) > 0 ) {
$sectionanchor = $wgParser->guessSectionNameFromWikiText( $matches[2] );
}
}
- wfProfileOut( "$fname-sectionanchor" );
+ wfProfileOut( __METHOD__ . '-sectionanchor' );
// Save errors may fall down to the edit form, but we've now
// merged the section into full text. Clear the section field
@@ -1015,26 +1035,26 @@ class EditPage {
$this->section = '';
// Check for length errors again now that the section is merged in
- $this->kblength = (int)(strlen( $text ) / 1024);
+ $this->kblength = (int)( strlen( $text ) / 1024 );
if ( $this->kblength > $wgMaxArticleSize ) {
$this->tooBig = true;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_MAX_ARTICLE_SIZE_EXCEEDED;
}
# update the article here
if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit,
- $this->watchthis, $bot, $sectionanchor ) )
+ $this->watchthis, $bot, $sectionanchor ) )
{
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_SUCCESS_UPDATE;
} else {
$this->isConflict = true;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return self::AS_END;
}
-
+
/**
* Check if no edits were made by other users since
* the time a user started editing the page. Limit to
@@ -1045,7 +1065,7 @@ class EditPage {
$dbw = wfGetDB( DB_MASTER );
$res = $dbw->select( 'revision',
'rev_user',
- array(
+ array(
'rev_page' => $this->mArticle->getId(),
'rev_timestamp > '.$dbw->addQuotes( $dbw->timestamp($edittime) )
),
@@ -1058,7 +1078,7 @@ class EditPage {
}
return true;
}
-
+
/**
* Check given input text against $wgSpamRegex, and return the text of the first match.
* @return mixed -- matching string or false
@@ -1069,7 +1089,7 @@ class EditPage {
$regexes = (array)$wgSpamRegex;
return self::matchSpamRegexInternal( $text, $regexes );
}
-
+
/**
* Check given input text against $wgSpamRegex, and return the text of the first match.
* @return mixed -- matching string or false
@@ -1079,7 +1099,7 @@ class EditPage {
$regexes = (array)$wgSummarySpamRegex;
return self::matchSpamRegexInternal( $text, $regexes );
}
-
+
protected static function matchSpamRegexInternal( $text, $regexes ) {
foreach( $regexes as $regex ) {
$matches = array();
@@ -1093,10 +1113,25 @@ class EditPage {
/**
* Initialise form fields in the object
* Called on the first invocation, e.g. when a user clicks an edit link
+ * @returns bool -- if the requested section is valid
*/
function initialiseForm() {
+ global $wgUser;
$this->edittime = $this->mArticle->getTimestamp();
$this->textbox1 = $this->getContent( false );
+ // activate checkboxes if user wants them to be always active
+ # Sort out the "watch" checkbox
+ if ( $wgUser->getOption( 'watchdefault' ) ) {
+ # Watch all edits
+ $this->watchthis = true;
+ } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
+ # Watch creations
+ $this->watchthis = true;
+ } elseif ( $this->mTitle->userIsWatching() ) {
+ # Already watched
+ $this->watchthis = true;
+ }
+ if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true;
if ( $this->textbox1 === false ) return false;
wfProxyCheck();
return true;
@@ -1115,7 +1150,7 @@ class EditPage {
$wgOut->setPageTitle( wfMsg( $msg, $wgTitle->getPrefixedText() ) );
} else {
# Use the title defined by DISPLAYTITLE magic word when present
- if ( isset($this->mParserOutput)
+ if ( isset( $this->mParserOutput )
&& ( $dt = $this->mParserOutput->getDisplayTitle() ) !== false ) {
$title = $dt;
} else {
@@ -1132,22 +1167,19 @@ class EditPage {
* near the top, for captchas and the like.
*/
function showEditForm( $formCallback=null ) {
- global $wgOut, $wgUser, $wgLang, $wgContLang, $wgMaxArticleSize, $wgTitle, $wgRequest;
+ global $wgOut, $wgUser, $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))
+ if ( is_null( $wgTitle ) )
return;
- $fname = 'EditPage::showEditForm';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$sk = $wgUser->getSkin();
- wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) ) ;
-
#need to parse the preview early so that we know which templates are used,
#otherwise users with "show preview after edit box" will get a blank list
#we parse this near the beginning so that setHeaders can do the title
@@ -1157,18 +1189,158 @@ class EditPage {
$previewOutput = $this->getPreviewText();
}
+ wfRunHooks( 'EditPage::showEditForm:initial', array( &$this ) );
+
$this->setHeaders();
# Enabled article-related sidebar, toplinks, etc.
$wgOut->setArticleRelated( true );
+ if ( $this->showHeader() === false )
+ return;
+
+ $action = htmlspecialchars($this->getActionURL($wgTitle));
+
+ if ( $wgUser->getOption( 'showtoolbar' ) and !$this->isCssJsSubpage ) {
+ # prepare toolbar for edit buttons
+ $toolbar = EditPage::getEditToolbar();
+ } else {
+ $toolbar = '';
+ }
+
+
+ $wgOut->addHTML( $this->editFormPageTop );
+
+ if ( $wgUser->getOption( 'previewontop' ) ) {
+ $this->displayPreviewArea( $previewOutput, true );
+ }
+
+ $wgOut->addHTML( $this->editFormTextTop );
+
+ $templates = $this->getTemplates();
+ $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != '');
+
+ $hiddencats = $this->mArticle->getHiddenCategories();
+ $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats );
+
+ if ( $this->wasDeletedSinceLastEdit() && 'save' != $this->formtype ) {
+ $wgOut->wrapWikiMsg(
+ "<div class='error mw-deleted-while-editing'>\n$1</div>",
+ 'deletedwhileediting' );
+ } elseif ( $this->wasDeletedSinceLastEdit() ) {
+ // Hide the toolbar and edit area, user can click preview to get it back
+ // Add an confirmation checkbox and explanation.
+ $toolbar = '';
+ // @todo move this to a cleaner conditional instead of blanking a variable
+ }
+ $wgOut->addHTML( <<<HTML
+{$toolbar}
+<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data">
+HTML
+);
+
+ if ( is_callable( $formCallback ) ) {
+ call_user_func_array( $formCallback, array( &$wgOut ) );
+ }
+
+ wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) );
+
+ // Put these up at the top to ensure they aren't lost on early form submission
+ $this->showFormBeforeText();
+
+ if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
+ $wgOut->addHTML(
+ '<div class="mw-confirm-recreate">' .
+ $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) .
+ Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false,
+ array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' )
+ ) .
+ '</div>'
+ );
+ }
+
+ # 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.
+ if ( $this->missingSummary ||
+ ( $this->section == 'new' && $this->nosummary ) )
+ $wgOut->addHTML( Xml::hidden( 'wpIgnoreBlankSummary', true ) );
+ $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
+ $wgOut->addHTML( Xml::hidden( 'wpAutoSummary', $autosumm ) );
+
+ $wgOut->addHTML( Xml::hidden( 'oldid', $this->mArticle->getOldID() ) );
+
+ if ( $this->section == 'new' ) {
+ $this->showSummaryInput( true, $this->summary );
+ $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
+ }
+
+ $wgOut->addHTML( $this->editFormTextBeforeContent );
+
if ( $this->isConflict ) {
- $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1</div>", 'explainconflict' );
+ // In an edit conflict bypass the overrideable content form method
+ // and fallback to the raw wpTextbox1 since editconflicts can't be
+ // resolved between page source edits and custom ui edits using the
+ // custom edit ui.
+ $this->showTextbox1( null, $this->getContent() );
+ } else {
+ $this->showContentForm();
+ }
+
+ $wgOut->addHTML( $this->editFormTextAfterContent );
+
+ $wgOut->addWikiText( $this->getCopywarn() );
+ if ( isset($this->editFormTextAfterWarn) && $this->editFormTextAfterWarn !== '' )
+ $wgOut->addHTML( $this->editFormTextAfterWarn );
+
+ $this->showStandardInputs();
+
+ $this->showFormAfterText();
+
+ $this->showTosSummary();
+ $this->showEditTools();
+
+ $wgOut->addHTML( <<<HTML
+{$this->editFormTextAfterTools}
+<div class='templatesUsed'>
+{$formattedtemplates}
+</div>
+<div class='hiddencats'>
+{$formattedhiddencats}
+</div>
+HTML
+);
+
+ if ( $this->isConflict )
+ $this->showConflict();
+
+ $wgOut->addHTML( $this->editFormTextBottom );
+ $wgOut->addHTML( "</form>\n" );
+ if ( !$wgUser->getOption( 'previewontop' ) ) {
+ $this->displayPreviewArea( $previewOutput, false );
+ }
- $this->textbox2 = $this->textbox1;
- $this->textbox1 = $this->getContent();
+ wfProfileOut( __METHOD__ );
+ }
+
+ protected function showHeader() {
+ global $wgOut, $wgUser, $wgTitle, $wgMaxArticleSize, $wgLang;
+ if ( $this->isConflict ) {
+ $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1</div>", 'explainconflict' );
$this->edittime = $this->mArticle->getTimestamp();
} else {
+ if ( $this->section != '' && !$this->isSectionEditSupported() ) {
+ // We use $this->section to much before this and getVal('wgSection') directly in other places
+ // at this point we can't reset $this->section to '' to fallback to non-section editing.
+ // Someone is welcome to try refactoring though
+ $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
+ return false;
+ }
+
if ( $this->section != '' && $this->section != 'new' ) {
$matches = array();
if ( !$this->summary && !$this->preview && !$this->diff ) {
@@ -1183,15 +1355,15 @@ class EditPage {
}
if ( $this->missingComment ) {
- $wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' );
+ $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1</div>", 'missingcommenttext' );
}
if ( $this->missingSummary && $this->section != 'new' ) {
- $wgOut->wrapWikiMsg( '<div id="mw-missingsummary">$1</div>', 'missingsummary' );
+ $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1</div>", 'missingsummary' );
}
if ( $this->missingSummary && $this->section == 'new' ) {
- $wgOut->wrapWikiMsg( '<div id="mw-missingcommentheader">$1</div>', 'missingcommentheader' );
+ $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1</div>", 'missingcommentheader' );
}
if ( $this->hookError !== '' ) {
@@ -1201,6 +1373,7 @@ class EditPage {
if ( !$this->checkUnicodeCompliantBrowser() ) {
$wgOut->addWikiMsg( 'nonunicodebrowser' );
}
+
if ( isset( $this->mArticle ) && isset( $this->mArticle->mRevision ) ) {
// Let sysop know that this will make private content public if saved
@@ -1220,41 +1393,37 @@ class EditPage {
if ( wfReadOnly() ) {
$wgOut->wrapWikiMsg( "<div id=\"mw-read-only-warning\">\n$1\n</div>", array( 'readonlywarning', wfReadOnlyReason() ) );
} elseif ( $wgUser->isAnon() && $this->formtype != 'preview' ) {
- $wgOut->wrapWikiMsg( '<div id="mw-anon-edit-warning">$1</div>', 'anoneditwarning' );
+ $wgOut->wrapWikiMsg( "<div id=\"mw-anon-edit-warning\">\n$1</div>", 'anoneditwarning' );
} else {
if ( $this->isCssJsSubpage ) {
# Check the skin exists
- if ( $this->isValidCssJsSubpage ) {
- if ( $this->formtype !== 'preview' ) {
- $wgOut->addWikiMsg( 'usercssjsyoucanpreview' );
- }
- } else {
+ if ( !$this->isValidCssJsSubpage ) {
$wgOut->addWikiMsg( 'userinvalidcssjstitle', $wgTitle->getSkinFromCssJsSubpage() );
}
+ if ( $this->formtype !== 'preview' ) {
+ if ( $this->isCssSubpage )
+ $wgOut->addWikiMsg( 'usercssyoucanpreview' );
+ if ( $this->isJsSubpage )
+ $wgOut->addWikiMsg( 'userjsyoucanpreview' );
+ }
}
}
- $classes = array(); // Textarea CSS
- if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
- } elseif ( $this->mTitle->isProtected( 'edit' ) ) {
+ if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
# Is the title semi-protected?
if ( $this->mTitle->isSemiProtected() ) {
$noticeMsg = 'semiprotectedpagewarning';
- $classes[] = 'mw-textarea-sprotected';
} else {
# Then it must be protected based on static groups (regular)
$noticeMsg = 'protectedpagewarning';
- $classes[] = 'mw-textarea-protected';
}
- $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
- $wgOut->addWikiMsg( $noticeMsg );
- LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '', 1 );
- $wgOut->addHTML( "</div>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '',
+ array( 'lim' => 1, 'msgKey' => array( $noticeMsg ) ) );
}
if ( $this->mTitle->isCascadeProtected() ) {
# Is this page under cascading protection from some source pages?
list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources();
- $notice = "<div class='mw-cascadeprotectedwarning'>$1\n";
+ $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
$cascadeSourcesCount = count( $cascadeSources );
if ( $cascadeSourcesCount > 0 ) {
# Explain, and list the titles responsible
@@ -1266,12 +1435,17 @@ class EditPage {
$wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) );
}
if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
- $wgOut->wrapWikiMsg( '<div class="mw-titleprotectedwarning">$1</div>', 'titleprotectedwarning' );
+ LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle->getPrefixedText(), '',
+ array( 'lim' => 1,
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'titleprotectedwarning' ),
+ 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ) );
}
if ( $this->kblength === false ) {
- $this->kblength = (int)(strlen( $this->textbox1 ) / 1024);
+ $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
}
+
if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
$wgOut->addHTML( "<div class='error' id='mw-edit-longpageerror'>\n" );
$wgOut->addWikiMsg( 'longpageerror', $wgLang->formatNum( $this->kblength ), $wgLang->formatNum( $wgMaxArticleSize ) );
@@ -1281,245 +1455,109 @@ class EditPage {
$wgOut->addWikiMsg( 'longpagewarning', $wgLang->formatNum( $this->kblength ) );
$wgOut->addHTML( "</div>\n" );
}
+ }
- $q = 'action='.$this->action;
- #if ( "no" == $redirect ) { $q .= "&redirect=no"; }
- $action = $wgTitle->escapeLocalURL( $q );
-
- $summary = wfMsg( 'summary' );
- $subject = wfMsg( 'subject' );
-
- $cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(),
- wfMsgExt('cancel', array('parseinline')) );
- $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' );
- $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ));
- $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'.
- htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '.
- htmlspecialchars( wfMsg( 'newwindow' ) );
-
- global $wgRightsText;
- if ( $wgRightsText ) {
- $copywarnMsg = array( 'copyrightwarning',
- '[[' . wfMsgForContent( 'copyrightpage' ) . ']]',
- $wgRightsText );
- } else {
- $copywarnMsg = array( 'copyrightwarning2',
- '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
- }
-
- if ( $wgUser->getOption('showtoolbar') and !$this->isCssJsSubpage ) {
- # prepare toolbar for edit buttons
- $toolbar = EditPage::getEditToolbar();
- } else {
- $toolbar = '';
- }
-
- // activate checkboxes if user wants them to be always active
- if ( !$this->preview && !$this->diff ) {
- # Sort out the "watch" checkbox
- if ( $wgUser->getOption( 'watchdefault' ) ) {
- # Watch all edits
- $this->watchthis = true;
- } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
- # Watch creations
- $this->watchthis = true;
- } elseif ( $this->mTitle->userIsWatching() ) {
- # Already watched
- $this->watchthis = true;
- }
-
- # May be overriden by request parameters
- if( $wgRequest->getBool( 'watchthis' ) ) {
- $this->watchthis = true;
- }
-
- if ( $wgUser->getOption( 'minordefault' ) ) $this->minoredit = true;
- }
-
- $wgOut->addHTML( $this->editFormPageTop );
+ /**
+ * Standard summary input and label (wgSummary), abstracted so EditPage
+ * subclasses may reorganize the form.
+ * Note that you do not need to worry about the label's for=, it will be
+ * inferred by the id given to the input. You can remove them both by
+ * passing array( 'id' => false ) to $userInputAttrs.
+ *
+ * @param $summary The value of the summary input
+ * @param $labelText The html to place inside the label
+ * @param $userInputAttrs An array of attrs to use on the input
+ * @param $userSpanAttrs An array of attrs to use on the span inside the label
+ *
+ * @return array An array in the format array( $label, $input )
+ */
+ function getSummaryInput($summary = "", $labelText = null, $inputAttrs = null, $spanLabelAttrs = null) {
+ $inputAttrs = ( is_array($inputAttrs) ? $inputAttrs : array() ) + array(
+ 'id' => 'wpSummary',
+ 'maxlength' => '200',
+ 'tabindex' => '1',
+ 'size' => 60,
+ 'spellcheck' => 'true',
+ );
+
+ $spanLabelAttrs = ( is_array($spanLabelAttrs) ? $spanLabelAttrs : array() ) + array(
+ 'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
+ 'id' => "wpSummaryLabel"
+ );
- if ( $wgUser->getOption( 'previewontop' ) ) {
- $this->displayPreviewArea( $previewOutput, true );
+ $label = null;
+ if ( $labelText ) {
+ $label = Xml::tags( 'label', $inputAttrs['id'] ? array( 'for' => $inputAttrs['id'] ) : null, $labelText );
+ $label = Xml::tags( 'span', $spanLabelAttrs, $label );
}
+ $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
- $wgOut->addHTML( $this->editFormTextTop );
-
- # 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 = $wgContLang->recodeForEdit( $this->summary );
+ return array( $label, $input );
+ }
- # 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 = '';
- if ( !$wgRequest->getBool( 'nosummary' ) ) {
- # Add a class if 'missingsummary' is triggered to allow styling of the summary line
- $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
-
- $commentsubject =
- Xml::tags( 'label', array( 'for' => 'wpSummary' ), $subject );
- $commentsubject =
- Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ),
- $commentsubject );
- $commentsubject .= '&nbsp;';
- $commentsubject .= Xml::input( 'wpSummary',
- 60,
- $summarytext,
- array(
- 'id' => 'wpSummary',
- 'maxlength' => '200',
- 'tabindex' => '1'
- ) );
- }
- $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 = '';
+ /**
+ * @param bool $isSubjectPreview true if this is the section subject/title
+ * up top, or false if this is the comment
+ * summary down below the textarea
+ * @param string $summary The text of the summary to display
+ * @return string
+ */
+ protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
+ global $wgOut, $wgContLang;
+ # Add a class if 'missingsummary' is triggered to allow styling of the summary line
+ $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
+ if ( $isSubjectPreview ) {
+ if ( $this->nosummary )
+ return;
} else {
- $commentsubject = '';
-
- # Add a class if 'missingsummary' is triggered to allow styling of the summary line
- $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
-
- $editsummary = Xml::tags( 'label', array( 'for' => 'wpSummary' ), $summary );
- $editsummary = Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ),
- $editsummary ) . ' ';
-
- $editsummary .= Xml::input( 'wpSummary',
- 60,
- $summarytext,
- array(
- 'id' => 'wpSummary',
- 'maxlength' => '200',
- 'tabindex' => '1'
- ) );
-
- // No idea where this is closed.
- $editsummary = Xml::openElement( 'div', array( 'class' => 'editOptions' ) )
- . $editsummary . '<br/>';
-
- $summarypreview = '';
- if ( $summarytext && $this->preview ) {
- $summarypreview =
- Xml::tags( 'div',
- array( 'class' => 'mw-summary-preview' ),
- wfMsg( 'summary-preview' ) .
- $sk->commentBlock( $this->summary, $this->mTitle )
- );
- }
- $subjectpreview = '';
- }
- $commentsubject .= $summaryhiddens;
-
- # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display
- if ( !$this->preview && !$this->diff ) {
- $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' );
- }
- $templates = $this->getTemplates();
- $formattedtemplates = $sk->formatTemplates( $templates, $this->preview, $this->section != '');
-
- $hiddencats = $this->mArticle->getHiddenCategories();
- $formattedhiddencats = $sk->formatHiddenCategories( $hiddencats );
-
- global $wgUseMetadataEdit ;
- if ( $wgUseMetadataEdit ) {
- $metadata = $this->mMetaData ;
- $metadata = htmlspecialchars( $wgContLang->recodeForEdit( $metadata ) ) ;
- $top = wfMsgWikiHtml( 'metadata_help' );
- /* ToDo: Replace with clean code */
- $ew = $wgUser->getOption( 'editwidth' );
- if ( $ew ) $ew = " style=\"width:100%\"";
- else $ew = '';
- $cols = $wgUser->getIntOption( 'cols' );
- /* /ToDo */
- $metadata = $top . "<textarea name='metadata' rows='3' cols='{$cols}'{$ew}>{$metadata}</textarea>" ;
- }
- else $metadata = "" ;
-
- $recreate = '';
- if ( $this->wasDeletedSinceLastEdit() ) {
- if ( 'save' != $this->formtype ) {
- $wgOut->wrapWikiMsg(
- "<div class='error mw-deleted-while-editing'>\n$1</div>",
- 'deletedwhileediting' );
- } else {
- // Hide the toolbar and edit area, user can click preview to get it back
- // Add an confirmation checkbox and explanation.
- $toolbar = '';
- $recreate = '<div class="mw-confirm-recreate">' .
- $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) .
- Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false,
- array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' )
- ) . '</div>';
- }
- }
-
- $tabindex = 2;
-
- $checkboxes = $this->getCheckboxes( $tabindex, $sk,
- array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) );
-
- $checkboxhtml = implode( $checkboxes, "\n" );
-
- $buttons = $this->getEditButtons( $tabindex );
- $buttonshtml = implode( $buttons, "\n" );
-
- $safemodehtml = $this->checkUnicodeCompliantBrowser()
- ? '' : Xml::hidden( 'safemode', '1' );
-
- $wgOut->addHTML( <<<END
-{$toolbar}
-<form id="editform" name="editform" method="post" action="$action" enctype="multipart/form-data">
-END
-);
-
- if ( is_callable( $formCallback ) ) {
- call_user_func_array( $formCallback, array( &$wgOut ) );
+ if ( !$this->mShowSummaryField )
+ return;
}
+ $summary = $wgContLang->recodeForEdit( $summary );
+ $labelText = wfMsgExt( $isSubjectPreview ? 'subject' : 'summary', 'parseinline' );
+ list($label, $input) = $this->getSummaryInput($summary, $labelText, array( 'class' => $summaryClass ), array());
+ $wgOut->addHTML("{$label} {$input}");
+ }
- wfRunHooks( 'EditPage::showEditForm:fields', array( &$this, &$wgOut ) );
-
- // Put these up at the top to ensure they aren't lost on early form submission
- $this->showFormBeforeText();
-
- $wgOut->addHTML( <<<END
-{$recreate}
-{$commentsubject}
-{$subjectpreview}
-{$this->editFormTextBeforeContent}
-END
-);
- $this->showTextbox1( $classes );
-
- $wgOut->wrapWikiMsg( "<div id=\"editpage-copywarn\">\n$1\n</div>", $copywarnMsg );
- $wgOut->addHTML( <<<END
-{$this->editFormTextAfterWarn}
-{$metadata}
-{$editsummary}
-{$summarypreview}
-{$checkboxhtml}
-{$safemodehtml}
-END
-);
+ /**
+ * @param bool $isSubjectPreview true if this is the section subject/title
+ * up top, or false if this is the comment
+ * summary down below the textarea
+ * @param string $summary The text of the summary to display
+ * @return string
+ */
+ protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
+ if ( !$summary || ( !$this->preview && !$this->diff ) )
+ return "";
+
+ global $wgParser, $wgUser;
+ $sk = $wgUser->getSkin();
+
+ if ( $isSubjectPreview )
+ $summary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $summary ) );
- $wgOut->addHTML(
-"<div class='editButtons'>
-{$buttonshtml}
- <span class='editHelp'>{$cancel}{$separator}{$edithelp}</span>
-</div><!-- editButtons -->
-</div><!-- editOptions -->");
+ $summary = wfMsgExt( 'subject-preview', 'parseinline' ) . $sk->commentBlock( $summary, $this->mTitle, !!$isSubjectPreview );
+ return Xml::tags( 'div', array( 'class' => 'mw-summary-preview' ), $summary );
+ }
+ protected function showFormBeforeText() {
+ global $wgOut;
+ $section = htmlspecialchars( $this->section );
+ $wgOut->addHTML( <<<INPUTS
+<input type='hidden' value="{$section}" name="wpSection" />
+<input type='hidden' value="{$this->starttime}" name="wpStarttime" />
+<input type='hidden' value="{$this->edittime}" name="wpEdittime" />
+<input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
+
+INPUTS
+ );
+ if ( !$this->checkUnicodeCompliantBrowser() )
+ $wgOut->addHTML(Xml::hidden( 'safemode', '1' ));
+ }
+
+ protected function showFormAfterText() {
+ global $wgOut, $wgUser;
/**
* To make it harder for someone to slip a user a page
* which submits an edit form to the wiki without their
@@ -1532,68 +1570,64 @@ END
* include the constant suffix to prevent editing from
* broken text-mangling proxies.
*/
- $token = htmlspecialchars( $wgUser->editToken() );
- $wgOut->addHTML( "\n<input type='hidden' value=\"$token\" name=\"wpEditToken\" />\n" );
-
- $this->showEditTools();
-
- $wgOut->addHTML( <<<END
-{$this->editFormTextAfterTools}
-<div class='templatesUsed'>
-{$formattedtemplates}
-</div>
-<div class='hiddencats'>
-{$formattedhiddencats}
-</div>
-END
-);
-
- if ( $this->isConflict && wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
- $wgOut->wrapWikiMsg( '==$1==', "yourdiff" );
-
- $de = new DifferenceEngine( $this->mTitle );
- $de->setText( $this->textbox2, $this->textbox1 );
- $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) );
-
- $wgOut->wrapWikiMsg( '==$1==', "yourtext" );
- $this->showTextbox2();
- }
- $wgOut->addHTML( $this->editFormTextBottom );
- $wgOut->addHTML( "</form>\n" );
- if ( !$wgUser->getOption( 'previewontop' ) ) {
- $this->displayPreviewArea( $previewOutput, false );
- }
-
- wfProfileOut( $fname );
+ $wgOut->addHTML( "\n" . Xml::hidden( "wpEditToken", $wgUser->editToken() ) . "\n" );
}
- protected function showFormBeforeText() {
- global $wgOut;
- $wgOut->addHTML( "
-<input type='hidden' value=\"" . htmlspecialchars( $this->section ) . "\" name=\"wpSection\" />
-<input type='hidden' value=\"{$this->starttime}\" name=\"wpStarttime\" />\n
-<input type='hidden' value=\"{$this->edittime}\" name=\"wpEdittime\" />\n
-<input type='hidden' value=\"{$this->scrolltop}\" name=\"wpScrolltop\" id=\"wpScrolltop\" />\n" );
+ /**
+ * Subpage overridable method for printing the form for page content editing
+ * By default this simply outputs wpTextbox1
+ * Subclasses can override this to provide a custom UI for editing;
+ * be it a form, or simply wpTextbox1 with a modified content that will be
+ * reverse modified when extracted from the post data.
+ * Note that this is basically the inverse for importContentFormData
+ *
+ * @praram WebRequest $request
+ */
+ protected function showContentForm() {
+ $this->showTextbox1();
}
-
- protected function showTextbox1( $classes ) {
+
+ /**
+ * Method to output wpTextbox1
+ * The $textoverride method can be used by subclasses overriding showContentForm
+ * to pass back to this method.
+ *
+ * @param array $customAttribs An array of html attributes to use in the textarea
+ * @param string $textoverride Optional text to override $this->textarea1 with
+ */
+ protected function showTextbox1($customAttribs = null, $textoverride = null) {
+ $classes = array(); // Textarea CSS
+ if ( $this->mTitle->getNamespace() != NS_MEDIAWIKI && $this->mTitle->isProtected( 'edit' ) ) {
+ # Is the title semi-protected?
+ if ( $this->mTitle->isSemiProtected() ) {
+ $classes[] = 'mw-textarea-sprotected';
+ } else {
+ # Then it must be protected based on static groups (regular)
+ $classes[] = 'mw-textarea-protected';
+ }
+ }
$attribs = array( 'tabindex' => 1 );
-
+ if ( is_array($customAttribs) )
+ $attribs += $customAttribs;
+
if ( $this->wasDeletedSinceLastEdit() )
$attribs['type'] = 'hidden';
- if ( !empty($classes) )
- $attribs['class'] = implode(' ',$classes);
+ if ( !empty( $classes ) ) {
+ if ( isset($attribs['class']) )
+ $classes[] = $attribs['class'];
+ $attribs['class'] = implode( ' ', $classes );
+ }
- $this->showTextbox( $this->textbox1, 'wpTextbox1', $attribs );
+ $this->showTextbox( isset($textoverride) ? $textoverride : $this->textbox1, 'wpTextbox1', $attribs );
}
-
+
protected function showTextbox2() {
$this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6 ) );
}
-
- protected function showTextbox( $content, $name, $attribs = array() ) {
+
+ protected function showTextbox( $content, $name, $customAttribs = array() ) {
global $wgOut, $wgUser;
-
+
$wikitext = $this->safeUnicodeOutput( $content );
if ( $wikitext !== '' ) {
// Ensure there's a newline at the end, otherwise adding lines
@@ -1602,18 +1636,19 @@ END
// mode will show an extra newline. A bit annoying.
$wikitext .= "\n";
}
-
- $attribs['accesskey'] = ',';
- $attribs['id'] = $name;
-
+
+ $attribs = $customAttribs + array(
+ 'accesskey' => ',',
+ 'id' => $name,
+ 'cols' => $wgUser->getIntOption( 'cols' ),
+ 'rows' => $wgUser->getIntOption( 'rows' ),
+ 'style' => '' // avoid php notices when appending for editwidth preference (appending allows customAttribs['style'] to still work
+ );
+
if ( $wgUser->getOption( 'editwidth' ) )
- $attribs['style'] = 'width: 100%';
-
- $wgOut->addHTML( Xml::textarea(
- $name,
- $wikitext,
- $wgUser->getIntOption( 'cols' ), $wgUser->getIntOption( 'rows' ),
- $attribs ) );
+ $attribs['style'] .= 'width: 100%';
+
+ $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
}
protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
@@ -1660,23 +1695,22 @@ END
}
}
- /**
- * Live Preview lets us fetch rendered preview page content and
- * add it to the page without refreshing the whole page.
- * If not supported by the browser it will fall through to the normal form
- * submission method.
- *
- * This function outputs a script tag to support live preview, and
- * returns an onclick handler which should be added to the attributes
- * of the preview button
- */
- function doLivePreviewScript() {
- global $wgOut, $wgTitle;
- $wgOut->addScriptFile( 'preview.js' );
- $liveAction = $wgTitle->getLocalUrl( "action={$this->action}&wpPreview=true&live=true" );
- return "return !lpDoPreview(" .
- "editform.wpTextbox1.value," .
- '"' . $liveAction . '"' . ")";
+ protected function showTosSummary() {
+ $msg = 'editpage-tos-summary';
+ // Give a chance for site and per-namespace customizations of
+ // terms of service summary link that might exist separately
+ // from the copyright notice.
+ //
+ // This will display between the save button and the edit tools,
+ // so should remain short!
+ wfRunHooks( 'EditPageTosSummary', array( $this->mTitle, &$msg ) );
+ $text = wfMsg( $msg );
+ if( !wfEmptyMsg( $msg, $text ) && $text !== '-' ) {
+ global $wgOut;
+ $wgOut->addHTML( '<div class="mw-tos-summary">' );
+ $wgOut->addWikiMsgArray( $msg, array() );
+ $wgOut->addHTML( '</div>' );
+ }
}
protected function showEditTools() {
@@ -1685,6 +1719,67 @@ END
$wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
$wgOut->addHTML( '</div>' );
}
+
+ protected function getCopywarn() {
+ global $wgRightsText;
+ if ( $wgRightsText ) {
+ $copywarnMsg = array( 'copyrightwarning',
+ '[[' . wfMsgForContent( 'copyrightpage' ) . ']]',
+ $wgRightsText );
+ } else {
+ $copywarnMsg = array( 'copyrightwarning2',
+ '[[' . wfMsgForContent( 'copyrightpage' ) . ']]' );
+ }
+ // Allow for site and per-namespace customization of contribution/copyright notice.
+ wfRunHooks( 'EditPageCopyrightWarning', array( $this->mTitle, &$copywarnMsg ) );
+
+ return "<div id=\"editpage-copywarn\">\n" . call_user_func_array("wfMsgNoTrans", $copywarnMsg) . "\n</div>";
+ }
+
+ protected function showStandardInputs( &$tabindex = 2 ) {
+ global $wgOut, $wgUser;
+ $wgOut->addHTML( "<div class='editOptions'>\n" );
+
+ if ( $this->section != 'new' ) {
+ $this->showSummaryInput( false, $this->summary );
+ $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
+ }
+
+ $checkboxes = $this->getCheckboxes( $tabindex, $wgUser->getSkin(),
+ array( 'minor' => $this->minoredit, 'watch' => $this->watchthis ) );
+ $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
+ $wgOut->addHTML( "<div class='editButtons'>\n" );
+ $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
+
+ $cancel = $this->getCancelLink();
+ $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' );
+ $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' ) );
+ $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'.
+ htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '.
+ htmlspecialchars( wfMsg( 'newwindow' ) );
+ $wgOut->addHTML( " <span class='editHelp'>{$cancel}{$separator}{$edithelp}</span>\n" );
+ $wgOut->addHTML( "</div><!-- editButtons -->\n</div><!-- editOptions -->\n" );
+ }
+
+ /*
+ * Show an edit conflict. textbox1 is already shown in showEditForm().
+ * If you want to use another entry point to this function, be careful.
+ */
+ protected function showConflict() {
+ global $wgOut;
+ $this->textbox2 = $this->textbox1;
+ $this->textbox1 = $this->getContent();
+ if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
+ $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+
+ $de = new DifferenceEngine( $this->mTitle );
+ $de->setText( $this->textbox2, $this->textbox1 );
+ $de->showDiff( wfMsg( "yourtext" ), wfMsg( "storedversion" ) );
+
+ $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+ $this->showTextbox2();
+ }
+ }
protected function getLastDelete() {
$dbr = wfGetDB( DB_SLAVE );
@@ -1698,7 +1793,7 @@ END
'log_title',
'log_comment',
'log_params',
- 'log_deleted',
+ 'log_deleted',
'user_name' ),
array( 'log_namespace' => $this->mTitle->getNamespace(),
'log_title' => $this->mTitle->getDBkey(),
@@ -1709,11 +1804,11 @@ END
array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' )
);
// Quick paranoid permission checks...
- if( is_object($data) ) {
+ if( is_object( $data ) ) {
if( $data->log_deleted & LogPage::DELETED_USER )
- $data->user_name = wfMsgHtml('rev-deleted-user');
+ $data->user_name = wfMsgHtml( 'rev-deleted-user' );
if( $data->log_deleted & LogPage::DELETED_COMMENT )
- $data->log_comment = wfMsgHtml('rev-deleted-comment');
+ $data->log_comment = wfMsgHtml( 'rev-deleted-comment' );
}
return $data;
}
@@ -1754,12 +1849,12 @@ END
# XXX: stupid php bug won't let us use $wgTitle->isCssJsSubpage() here
if ( $this->isCssJsSubpage ) {
- if (preg_match("/\\.css$/", $this->mTitle->getText() ) ) {
- $previewtext = wfMsg('usercsspreview');
- } else if (preg_match("/\\.js$/", $this->mTitle->getText() ) ) {
- $previewtext = wfMsg('userjspreview');
+ if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
+ $previewtext = wfMsg( 'usercsspreview' );
+ } else if (preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
+ $previewtext = wfMsg( 'userjspreview' );
}
- $parserOptions->setTidy(true);
+ $parserOptions->setTidy( true );
$parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
$previewHTML = $parserOutput->mText;
} elseif ( $rt = Title::newFromRedirectArray( $this->textbox1 ) ) {
@@ -1769,11 +1864,11 @@ END
# If we're adding a comment, we need to show the
# summary as the headline
- if ( $this->section=="new" && $this->summary!="" ) {
- $toparse="== {$this->summary} ==\n\n".$toparse;
+ if ( $this->section == "new" && $this->summary != "" ) {
+ $toparse = "== {$this->summary} ==\n\n" . $toparse;
}
- if ( $this->mMetaData != "" ) $toparse .= "\n" . $this->mMetaData;
+ wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
// Parse mediawiki messages with correct target language
if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
@@ -1783,7 +1878,7 @@ END
}
- $parserOptions->setTidy(true);
+ $parserOptions->setTidy( true );
$parserOptions->enableLimitReport();
$parserOutput = $wgParser->parse( $this->mArticle->preSaveTransform( $toparse ),
$this->mTitle, $parserOptions );
@@ -1797,20 +1892,24 @@ END
}
}
- $previewhead = '<h2>' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>\n" .
- "<div class='previewnote'>" . $wgOut->parse( $note ) . "</div>\n";
- if ( $this->isConflict ) {
- $previewhead .='<h2>' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
+ if( $this->isConflict ) {
+ $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
+ } else {
+ $conflict = '<hr />';
}
+ $previewhead = "<div class='previewnote'>\n" .
+ '<h2 id="mw-previewheader">' . htmlspecialchars( wfMsg( 'preview' ) ) . "</h2>" .
+ $wgOut->parse( $note ) . $conflict . "</div>\n";
+
wfProfileOut( __METHOD__ );
- return $previewhead . $previewHTML;
+ return $previewhead . $previewHTML . $this->previewTextAfterContent;
}
-
+
function getTemplates() {
if ( $this->preview || $this->section != '' ) {
$templates = array();
- if ( !isset($this->mParserOutput) ) return $templates;
+ if ( !isset( $this->mParserOutput ) ) return $templates;
foreach( $this->mParserOutput->getTemplates() as $ns => $template) {
foreach( array_keys( $template ) as $dbk ) {
$templates[] = Title::makeTitle($ns, $dbk);
@@ -1826,7 +1925,7 @@ END
* Call the stock "user is blocked" page
*/
function blockedPage() {
- global $wgOut, $wgUser;
+ global $wgOut;
$wgOut->blockedPage( false ); # Standard block notice on the top, don't 'return'
# If the user made changes, preserve them when showing the markup
@@ -1840,14 +1939,9 @@ END
# Spit out the source or the user's modified version
if ( $source !== false ) {
- $rows = $wgUser->getIntOption( 'rows' );
- $cols = $wgUser->getIntOption( 'cols' );
- $attribs = array( 'id' => 'wpTextbox1', 'name' => 'wpTextbox1', 'cols' => $cols, 'rows' => $rows, 'readonly' => 'readonly' );
$wgOut->addHTML( '<hr />' );
$wgOut->addWikiMsg( $first ? 'blockedoriginalsource' : 'blockededitsource', $this->mTitle->getPrefixedText() );
- # Why we don't use Xml::element here?
- # Is it because if $source is '', it returns <textarea />?
- $wgOut->addHTML( Xml::openElement( 'textarea', $attribs ) . htmlspecialchars( $source ) . Xml::closeElement( 'textarea' ) );
+ $this->showTextbox1( array( 'readonly' ), $source );
}
}
@@ -1859,7 +1953,13 @@ END
$skin = $wgUser->getSkin();
$loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
- $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() );
+ $loginLink = $skin->link(
+ $loginTitle,
+ wfMsgHtml( 'loginreqlink' ),
+ array(),
+ array( 'returnto' => $wgTitle->getPrefixedText() ),
+ array( 'known', 'noclasses' )
+ );
$wgOut->setPageTitle( wfMsg( 'whitelistedittitle' ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
@@ -1874,14 +1974,17 @@ END
* they have attempted to edit a nonexistent section.
*/
function noSuchSectionPage() {
- global $wgOut, $wgTitle;
+ global $wgOut;
$wgOut->setPageTitle( wfMsg( 'nosuchsectiontitle' ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
- $wgOut->addWikiMsg( 'nosuchsectiontext', $this->section );
- $wgOut->returnToMain( false, $wgTitle );
+ $res = wfMsgExt( 'nosuchsectiontext', 'parse', $this->section );
+ wfRunHooks( 'EditPageNoSuchSection', array( &$this, &$res ) );
+ $wgOut->addHTML( $res );
+
+ $wgOut->returnToMain( false, $this->mTitle );
}
/**
@@ -1910,15 +2013,14 @@ END
* @todo document
*/
function mergeChangesInto( &$editText ){
- $fname = 'EditPage::mergeChangesInto';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$db = wfGetDB( DB_MASTER );
// This is the revision the editor started from
$baseRevision = $this->getBaseRevision();
if ( is_null( $baseRevision ) ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
$baseText = $baseRevision->getText();
@@ -1926,7 +2028,7 @@ END
// The current state, we want to merge updates into it
$currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
if ( is_null( $currentRevision ) ) {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
$currentText = $currentRevision->getText();
@@ -1934,10 +2036,10 @@ END
$result = '';
if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
$editText = $result;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return true;
} else {
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return false;
}
}
@@ -1987,13 +2089,14 @@ 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 skins/common/edit.js.
- *
+ *
* @return string
*/
static function getEditToolbar() {
- global $wgStylePath, $wgContLang, $wgLang, $wgJsMimeType;
+ global $wgStylePath, $wgContLang, $wgLang;
/**
+
* toolarray an array of arrays which each include the filename of
* the button image (without path), the opening tag, the closing tag,
* and optionally a sample text that is inserted between the two when no
@@ -2006,111 +2109,111 @@ END
*/
$toolarray = array(
array(
- 'image' => $wgLang->getImageFile('button-bold'),
+ 'image' => $wgLang->getImageFile( 'button-bold' ),
'id' => 'mw-editbutton-bold',
'open' => '\'\'\'',
'close' => '\'\'\'',
- 'sample' => wfMsg('bold_sample'),
- 'tip' => wfMsg('bold_tip'),
+ 'sample' => wfMsg( 'bold_sample' ),
+ 'tip' => wfMsg( 'bold_tip' ),
'key' => 'B'
),
array(
- 'image' => $wgLang->getImageFile('button-italic'),
+ 'image' => $wgLang->getImageFile( 'button-italic' ),
'id' => 'mw-editbutton-italic',
'open' => '\'\'',
'close' => '\'\'',
- 'sample' => wfMsg('italic_sample'),
- 'tip' => wfMsg('italic_tip'),
+ 'sample' => wfMsg( 'italic_sample' ),
+ 'tip' => wfMsg( 'italic_tip' ),
'key' => 'I'
),
array(
- 'image' => $wgLang->getImageFile('button-link'),
+ 'image' => $wgLang->getImageFile( 'button-link' ),
'id' => 'mw-editbutton-link',
'open' => '[[',
'close' => ']]',
- 'sample' => wfMsg('link_sample'),
- 'tip' => wfMsg('link_tip'),
+ 'sample' => wfMsg( 'link_sample' ),
+ 'tip' => wfMsg( 'link_tip' ),
'key' => 'L'
),
array(
- 'image' => $wgLang->getImageFile('button-extlink'),
+ 'image' => $wgLang->getImageFile( 'button-extlink' ),
'id' => 'mw-editbutton-extlink',
'open' => '[',
'close' => ']',
- 'sample' => wfMsg('extlink_sample'),
- 'tip' => wfMsg('extlink_tip'),
+ 'sample' => wfMsg( 'extlink_sample' ),
+ 'tip' => wfMsg( 'extlink_tip' ),
'key' => 'X'
),
array(
- 'image' => $wgLang->getImageFile('button-headline'),
+ 'image' => $wgLang->getImageFile( 'button-headline' ),
'id' => 'mw-editbutton-headline',
'open' => "\n== ",
'close' => " ==\n",
- 'sample' => wfMsg('headline_sample'),
- 'tip' => wfMsg('headline_tip'),
+ 'sample' => wfMsg( 'headline_sample' ),
+ 'tip' => wfMsg( 'headline_tip' ),
'key' => 'H'
),
array(
- 'image' => $wgLang->getImageFile('button-image'),
+ 'image' => $wgLang->getImageFile( 'button-image' ),
'id' => 'mw-editbutton-image',
- 'open' => '[['.$wgContLang->getNsText(NS_FILE).':',
+ 'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
'close' => ']]',
- 'sample' => wfMsg('image_sample'),
- 'tip' => wfMsg('image_tip'),
+ 'sample' => wfMsg( 'image_sample' ),
+ 'tip' => wfMsg( 'image_tip' ),
'key' => 'D'
),
array(
- 'image' => $wgLang->getImageFile('button-media'),
+ 'image' => $wgLang->getImageFile( 'button-media' ),
'id' => 'mw-editbutton-media',
- 'open' => '[['.$wgContLang->getNsText(NS_MEDIA).':',
+ 'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
'close' => ']]',
- 'sample' => wfMsg('media_sample'),
- 'tip' => wfMsg('media_tip'),
+ 'sample' => wfMsg( 'media_sample' ),
+ 'tip' => wfMsg( 'media_tip' ),
'key' => 'M'
),
array(
- 'image' => $wgLang->getImageFile('button-math'),
+ 'image' => $wgLang->getImageFile( 'button-math' ),
'id' => 'mw-editbutton-math',
'open' => "<math>",
'close' => "</math>",
- 'sample' => wfMsg('math_sample'),
- 'tip' => wfMsg('math_tip'),
+ 'sample' => wfMsg( 'math_sample' ),
+ 'tip' => wfMsg( 'math_tip' ),
'key' => 'C'
),
array(
- 'image' => $wgLang->getImageFile('button-nowiki'),
+ 'image' => $wgLang->getImageFile( 'button-nowiki' ),
'id' => 'mw-editbutton-nowiki',
'open' => "<nowiki>",
'close' => "</nowiki>",
- 'sample' => wfMsg('nowiki_sample'),
- 'tip' => wfMsg('nowiki_tip'),
+ 'sample' => wfMsg( 'nowiki_sample' ),
+ 'tip' => wfMsg( 'nowiki_tip' ),
'key' => 'N'
),
array(
- 'image' => $wgLang->getImageFile('button-sig'),
+ 'image' => $wgLang->getImageFile( 'button-sig' ),
'id' => 'mw-editbutton-signature',
'open' => '--~~~~',
'close' => '',
'sample' => '',
- 'tip' => wfMsg('sig_tip'),
+ 'tip' => wfMsg( 'sig_tip' ),
'key' => 'Y'
),
array(
- 'image' => $wgLang->getImageFile('button-hr'),
+ 'image' => $wgLang->getImageFile( 'button-hr' ),
'id' => 'mw-editbutton-hr',
'open' => "\n----\n",
'close' => '',
'sample' => '',
- 'tip' => wfMsg('hr_tip'),
+ 'tip' => wfMsg( 'hr_tip' ),
'key' => 'R'
)
);
$toolbar = "<div id='toolbar'>\n";
- $toolbar.="<script type='$wgJsMimeType'>\n/*<![CDATA[*/\n";
- foreach($toolarray as $tool) {
+ $script = '';
+ foreach ( $toolarray as $tool ) {
$params = array(
- $image = $wgStylePath.'/common/images/'.$tool['image'],
+ $image = $wgStylePath . '/common/images/' . $tool['image'],
// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
// Older browsers show a "speedtip" type message only for ALT.
// Ideally these should be different, realistically they
@@ -2124,11 +2227,14 @@ END
$paramList = implode( ',',
array_map( array( 'Xml', 'encodeJsVar' ), $params ) );
- $toolbar.="addButton($paramList);\n";
+ $script .= "addButton($paramList);\n";
}
+ $toolbar .= Html::inlineScript( "\n$script\n" );
+
+ $toolbar .= "\n</div>";
+
+ wfRunHooks( 'EditPageBeforeEditToolbar', array( &$toolbar ) );
- $toolbar.="/*]]>*/\n</script>";
- $toolbar.="\n</div>";
return $toolbar;
}
@@ -2149,8 +2255,8 @@ END
$checkboxes = array();
$checkboxes['minor'] = '';
- $minorLabel = wfMsgExt('minoredit', array('parseinline'));
- if ( $wgUser->isAllowed('minoredit') ) {
+ $minorLabel = wfMsgExt( 'minoredit', array( 'parseinline' ) );
+ if ( $wgUser->isAllowed( 'minoredit' ) ) {
$attribs = array(
'tabindex' => ++$tabindex,
'accesskey' => wfMsg( 'accesskey-minoredit' ),
@@ -2158,10 +2264,10 @@ END
);
$checkboxes['minor'] =
Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
- "&nbsp;<label for='wpMinoredit'".$skin->tooltip('minoredit', 'withaccess').">{$minorLabel}</label>";
+ "&nbsp;<label for='wpMinoredit'" . $skin->tooltip( 'minoredit', 'withaccess' ) . ">{$minorLabel}</label>";
}
- $watchLabel = wfMsgExt('watchthis', array('parseinline'));
+ $watchLabel = wfMsgExt( 'watchthis', array( 'parseinline' ) );
$checkboxes['watch'] = '';
if ( $wgUser->isLoggedIn() ) {
$attribs = array(
@@ -2171,7 +2277,7 @@ END
);
$checkboxes['watch'] =
Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
- "&nbsp;<label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>";
+ "&nbsp;<label for='wpWatchthis'" . $skin->tooltip( 'watch', 'withaccess' ) . ">{$watchLabel}</label>";
}
wfRunHooks( 'EditPageBeforeEditChecks', array( &$this, &$checkboxes, &$tabindex ) );
return $checkboxes;
@@ -2186,8 +2292,6 @@ END
* @return array
*/
public function getEditButtons(&$tabindex) {
- global $wgLivePreview, $wgUser;
-
$buttons = array();
$temp = array(
@@ -2195,61 +2299,35 @@ END
'name' => 'wpSave',
'type' => 'submit',
'tabindex' => ++$tabindex,
- 'value' => wfMsg('savearticle'),
- 'accesskey' => wfMsg('accesskey-save'),
+ 'value' => wfMsg( 'savearticle' ),
+ 'accesskey' => wfMsg( 'accesskey-save' ),
'title' => wfMsg( 'tooltip-save' ).' ['.wfMsg( 'accesskey-save' ).']',
);
$buttons['save'] = Xml::element('input', $temp, '');
++$tabindex; // use the same for preview and live preview
- if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) {
- $temp = array(
- 'id' => 'wpPreview',
- 'name' => 'wpPreview',
- 'type' => 'submit',
- 'tabindex' => $tabindex,
- 'value' => wfMsg('showpreview'),
- 'accesskey' => '',
- 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']',
- 'style' => 'display: none;',
- );
- $buttons['preview'] = Xml::element('input', $temp, '');
-
- $temp = array(
- 'id' => 'wpLivePreview',
- 'name' => 'wpLivePreview',
- 'type' => 'submit',
- 'tabindex' => $tabindex,
- 'value' => wfMsg('showlivepreview'),
- 'accesskey' => wfMsg('accesskey-preview'),
- 'title' => '',
- 'onclick' => $this->doLivePreviewScript(),
- );
- $buttons['live'] = Xml::element('input', $temp, '');
- } else {
- $temp = array(
- 'id' => 'wpPreview',
- 'name' => 'wpPreview',
- 'type' => 'submit',
- 'tabindex' => $tabindex,
- 'value' => wfMsg('showpreview'),
- 'accesskey' => wfMsg('accesskey-preview'),
- 'title' => wfMsg( 'tooltip-preview' ).' ['.wfMsg( 'accesskey-preview' ).']',
- );
- $buttons['preview'] = Xml::element('input', $temp, '');
- $buttons['live'] = '';
- }
+ $temp = array(
+ 'id' => 'wpPreview',
+ 'name' => 'wpPreview',
+ 'type' => 'submit',
+ 'tabindex' => $tabindex,
+ 'value' => wfMsg( 'showpreview' ),
+ 'accesskey' => wfMsg( 'accesskey-preview' ),
+ 'title' => wfMsg( 'tooltip-preview' ) . ' [' . wfMsg( 'accesskey-preview' ) . ']',
+ );
+ $buttons['preview'] = Xml::element( 'input', $temp, '' );
+ $buttons['live'] = '';
$temp = array(
'id' => 'wpDiff',
'name' => 'wpDiff',
'type' => 'submit',
'tabindex' => ++$tabindex,
- 'value' => wfMsg('showdiff'),
- 'accesskey' => wfMsg('accesskey-diff'),
- 'title' => wfMsg( 'tooltip-diff' ).' ['.wfMsg( 'accesskey-diff' ).']',
+ 'value' => wfMsg( 'showdiff' ),
+ 'accesskey' => wfMsg( 'accesskey-diff' ),
+ 'title' => wfMsg( 'tooltip-diff' ) . ' [' . wfMsg( 'accesskey-diff' ) . ']',
);
- $buttons['diff'] = Xml::element('input', $temp, '');
+ $buttons['diff'] = Xml::element( 'input', $temp, '' );
wfRunHooks( 'EditPageBeforeEditButtons', array( &$this, &$buttons, &$tabindex ) );
return $buttons;
@@ -2286,6 +2364,23 @@ END
}
+ public function getCancelLink() {
+ global $wgUser, $wgTitle;
+
+ $cancelParams = array();
+ if ( !$this->isConflict && $this->mArticle->getOldID() > 0 ) {
+ $cancelParams['oldid'] = $this->mArticle->getOldID();
+ }
+
+ return $wgUser->getSkin()->link(
+ $wgTitle,
+ wfMsgExt( 'cancel', array( 'parseinline' ) ),
+ array( 'id' => 'mw-editform-cancel' ),
+ $cancelParams,
+ array( 'known', 'noclasses' )
+ );
+ }
+
/**
* Get a diff between the current contents of the edit box and the
* version of the page we're editing from.
@@ -2297,9 +2392,12 @@ END
$oldtext = $this->mArticle->fetchContent();
$newtext = $this->mArticle->replaceSection(
$this->section, $this->textbox1, $this->summary, $this->edittime );
+
+ wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
+
$newtext = $this->mArticle->preSaveTransform( $newtext );
- $oldtitle = wfMsgExt( 'currentrev', array('parseinline') );
- $newtitle = wfMsgExt( 'yourtext', array('parseinline') );
+ $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) );
+ $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
if ( $oldtext !== false || $newtext != '' ) {
$de = new DifferenceEngine( $this->mTitle );
$de->setText( $oldtext, $newtext );
@@ -2329,6 +2427,13 @@ END
: $text;
}
+ function safeUnicodeText( $request, $text ) {
+ $text = rtrim( $text );
+ return $request->getBool( 'safemode' )
+ ? $this->unmakesafe( $text )
+ : $text;
+ }
+
/**
* Filter an output field through a Unicode armoring process if it is
* going to an old browser with known broken Unicode editing issues.
@@ -2435,58 +2540,22 @@ END
}
/**
- * 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
- */
- protected function showDeletionLog( $out ) {
- global $wgUser;
- $loglist = new LogEventsList( $wgUser->getSkin(), $out );
- $pager = new LogPager( $loglist, 'delete', false, $this->mTitle->getPrefixedText() );
- $count = $pager->getNumRows();
- if ( $count > 0 ) {
- $pager->mLimit = 10;
- $out->addHTML( '<div class="mw-warning-with-logexcerpt">' );
- $out->addWikiMsg( 'recreate-deleted-warn' );
- $out->addHTML(
- $loglist->beginLogEventsList() .
- $pager->getBody() .
- $loglist->endLogEventsList()
- );
- if($count > 10){
- $out->addHTML( $wgUser->getSkin()->link(
- SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'deletelog-fulllog' ),
- array(),
- array(
- 'type' => 'delete',
- 'page' => $this->mTitle->getPrefixedText() ) ) );
- }
- $out->addHTML( '</div>' );
- return true;
- }
-
- return false;
- }
-
- /**
* Attempt submission
* @return bool false if output is done, true if the rest of the form should be displayed
*/
function attemptSave() {
- global $wgUser, $wgOut, $wgTitle, $wgRequest;
+ global $wgUser, $wgOut, $wgTitle;
$resultDetails = false;
# Allow bots to exempt some edits from bot flagging
- $bot = $wgUser->isAllowed('bot') && $wgRequest->getBool('bot',true);
+ $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
$value = $this->internalAttemptSave( $resultDetails, $bot );
if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) {
$this->didSave = true;
}
- switch ($value) {
+ switch ( $value ) {
case self::AS_HOOK_ERROR_EXPECTED:
case self::AS_CONTENT_TOO_BIG:
case self::AS_ARTICLE_WAS_DELETED:
@@ -2504,7 +2573,7 @@ END
return false;
case self::AS_SPAM_ERROR:
- $this->spamPage ( $resultDetails['spam'] );
+ $this->spamPage( $resultDetails['spam'] );
return false;
case self::AS_BLOCKED_PAGE_FOR_USER:
@@ -2528,7 +2597,7 @@ END
$wgOut->rateLimited();
return false;
- case self::AS_NO_CREATE_PERMISSION;
+ case self::AS_NO_CREATE_PERMISSION:
$this->noCreatePermission();
return;
@@ -2541,7 +2610,7 @@ END
return false;
}
}
-
+
function getBaseRevision() {
if ( $this->mBaseRevision == false ) {
$db = wfGetDB( DB_MASTER );
@@ -2551,5 +2620,5 @@ END
} else {
return $this->mBaseRevision;
}
- }
+ }
}
diff --git a/includes/Exception.php b/includes/Exception.php
index 5f808b20..f6bc6f87 100644
--- a/includes/Exception.php
+++ b/includes/Exception.php
@@ -8,13 +8,13 @@
* @ingroup Exception
*/
class MWException extends Exception {
-
/**
* Should the exception use $wgOut to output the error ?
* @return bool
*/
function useOutputPage() {
- return !empty( $GLOBALS['wgFullyInitialised'] ) &&
+ return $this->useMessageCache() &&
+ !empty( $GLOBALS['wgFullyInitialised'] ) &&
( !empty( $GLOBALS['wgArticle'] ) || ( !empty( $GLOBALS['wgOut'] ) && !$GLOBALS['wgOut']->isArticle() ) ) &&
!empty( $GLOBALS['wgTitle'] );
}
@@ -25,6 +25,11 @@ class MWException extends Exception {
*/
function useMessageCache() {
global $wgLang;
+ foreach ( $this->getTrace() as $frame ) {
+ if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) {
+ return false;
+ }
+ }
return is_object( $wgLang );
}
@@ -202,7 +207,7 @@ class MWException extends Exception {
header( 'Pragma: nocache' );
}
$title = $this->getPageTitle();
- echo "<html>
+ return "<html>
<head>
<title>$title</title>
</head>
@@ -215,7 +220,7 @@ class MWException extends Exception {
* print the end of the html page if not using $wgOut.
*/
function htmlFooter() {
- echo "</body></html>";
+ return "</body></html>";
}
/**
@@ -297,7 +302,7 @@ function wfReportException( Exception $e ) {
wfPrintError( $message );
} else {
echo nl2br( htmlspecialchars( $message ) ). "\n";
- }
+ }
}
} else {
$message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" .
@@ -322,8 +327,7 @@ function wfPrintError( $message ) {
# Try to produce meaningful output anyway. Using echo may corrupt output to STDOUT though.
if ( defined( 'STDERR' ) ) {
fwrite( STDERR, $message );
- }
- else {
+ } else {
echo( $message );
}
}
diff --git a/includes/Exif.php b/includes/Exif.php
index 9e54bd55..594e633a 100644
--- a/includes/Exif.php
+++ b/includes/Exif.php
@@ -558,7 +558,7 @@ class Exif {
* @param $fname String:
* @param $action Mixed: , default NULL.
*/
- function debug( $in, $fname, $action = NULL ) {
+ function debug( $in, $fname, $action = null ) {
if ( !$this->log ) {
return;
}
@@ -1043,6 +1043,14 @@ class FormatExif {
$this->formatNum( $val ) );
break;
+ // Do not transform fields with pure text.
+ // For some languages the formatNum() conversion results to wrong output like
+ // foo,bar@example,com or foo٫bar@example٫com
+ case 'ImageDescription':
+ case 'Artist':
+ case 'Copyright':
+ $tags[$tag] = htmlspecialchars( $val );
+ break;
default:
$tags[$tag] = $this->formatNum( $val );
break;
@@ -1080,11 +1088,13 @@ class FormatExif {
* @return mixed A floating point number or whatever we were fed
*/
function formatNum( $num ) {
+ global $wgLang;
+
$m = array();
if ( preg_match( '/^(\d+)\/(\d+)$/', $num, $m ) )
- return $m[2] != 0 ? $m[1] / $m[2] : $num;
+ return $wgLang->formatNum( $m[2] != 0 ? $m[1] / $m[2] : $num );
else
- return $num;
+ return $wgLang->formatNum( $num );
}
/**
@@ -1103,7 +1113,7 @@ class FormatExif {
$gcd = $this->gcd( $numerator, $denominator );
if( $gcd != 0 ) {
// 0 shouldn't happen! ;)
- return $numerator / $gcd . '/' . $denominator / $gcd;
+ return $this->formatNum( $numerator / $gcd ) . '/' . $this->formatNum( $denominator / $gcd );
}
}
return $this->formatNum( $num );
diff --git a/includes/Export.php b/includes/Export.php
index 909804cf..e8e74289 100644
--- a/includes/Export.php
+++ b/includes/Export.php
@@ -55,6 +55,7 @@ class WikiExporter {
* limit: maximum number of rows to return
* dir: "asc" or "desc" timestamp order
* @param $buffer Int: one of WikiExporter::BUFFER or WikiExporter::STREAM
+ * @param $text Int: one of WikiExporter::TEXT or WikiExporter::STUB
*/
function __construct( &$db, $history = WikiExporter::CURRENT,
$buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) {
@@ -155,7 +156,7 @@ class WikiExporter {
wfProfileIn( $fname );
$this->author_list = "<contributors>";
//rev_deleted
- $nothidden = '(rev_deleted & '.Revision::DELETED_USER.') = 0';
+ $nothidden = '('.$this->db->bitAnd('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 ;
@@ -197,37 +198,18 @@ class WikiExporter {
array( 'ORDER BY' => 'log_id', 'USE INDEX' => array('logging' => 'PRIMARY') )
);
$wrapper = $this->db->resultObject( $result );
+ $this->outputLogStream( $wrapper );
if( $this->buffer == WikiExporter::STREAM ) {
$this->db->bufferResults( $prev );
}
- $this->outputLogStream( $wrapper );
# For page dumps...
} else {
$tables = array( 'page', 'revision' );
$opts = array( 'ORDER BY' => 'page_id ASC' );
$opts['USE INDEX'] = array();
$join = array();
- # Full history dumps...
- if( $this->history & WikiExporter::FULL ) {
- $join['revision'] = array('INNER JOIN','page_id=rev_page');
- # Latest revision dumps...
- } elseif( $this->history & WikiExporter::CURRENT ) {
- if( $this->list_authors && $cond != '' ) { // List authors, if so desired
- list($page,$revision) = $this->db->tableNamesN('page','revision');
- $this->do_list_authors( $page, $revision, $cond );
- }
- $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id');
- # "Stable" revision dumps...
- } elseif( $this->history & WikiExporter::STABLE ) {
- # Default JOIN, to be overridden...
- $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id');
- # One, and only one hook should set this, and return false
- if( wfRunHooks( 'WikiExporter::dumpStableQuery', array(&$tables,&$opts,&$join) ) ) {
- wfProfileOut( __METHOD__ );
- return new WikiError( __METHOD__." given invalid history dump type." );
- }
- # Time offset/limit for all pages/history...
- } elseif( is_array( $this->history ) ) {
+ if( is_array( $this->history ) ) {
+ # Time offset/limit for all pages/history...
$revJoin = 'page_id=rev_page';
# Set time order
if( $this->history['dir'] == 'asc' ) {
@@ -247,8 +229,27 @@ class WikiExporter {
if( !empty( $this->history['limit'] ) ) {
$opts['LIMIT'] = intval( $this->history['limit'] );
}
- # Uknown history specification parameter?
+ } elseif( $this->history & WikiExporter::FULL ) {
+ # Full history dumps...
+ $join['revision'] = array('INNER JOIN','page_id=rev_page');
+ } elseif( $this->history & WikiExporter::CURRENT ) {
+ # Latest revision dumps...
+ if( $this->list_authors && $cond != '' ) { // List authors, if so desired
+ list($page,$revision) = $this->db->tableNamesN('page','revision');
+ $this->do_list_authors( $page, $revision, $cond );
+ }
+ $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id');
+ } elseif( $this->history & WikiExporter::STABLE ) {
+ # "Stable" revision dumps...
+ # Default JOIN, to be overridden...
+ $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id');
+ # One, and only one hook should set this, and return false
+ if( wfRunHooks( 'WikiExporter::dumpStableQuery', array(&$tables,&$opts,&$join) ) ) {
+ wfProfileOut( __METHOD__ );
+ return new WikiError( __METHOD__." given invalid history dump type." );
+ }
} else {
+ # Uknown history specification parameter?
wfProfileOut( __METHOD__ );
return new WikiError( __METHOD__." given invalid history dump type." );
}
@@ -266,6 +267,9 @@ class WikiExporter {
if( $this->buffer == WikiExporter::STREAM ) {
$prev = $this->db->bufferResults( false );
}
+
+ wfRunHooks( 'ModifyExportQuery',
+ array( $this->db, &$tables, &$cond, &$opts, &$join ) );
# Do the query!
$result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join );
@@ -325,7 +329,6 @@ class WikiExporter {
$output .= $this->writer->closePage();
$this->sink->writeClosePage( $output );
}
- $resultset->free();
}
protected function outputLogStream( $resultset ) {
@@ -333,7 +336,6 @@ class WikiExporter {
$output = $this->writer->writeLogItem( $row );
$this->sink->writeLogItem( $row, $output );
}
- $resultset->free();
}
}
@@ -347,7 +349,7 @@ class XmlDumpWriter {
* @return string
*/
function schemaVersion() {
- return "0.3"; // FIXME: upgrade to 0.4 when updated XSD is ready, for the revision deletion bits
+ return "0.4";
}
/**
@@ -412,7 +414,11 @@ class XmlDumpWriter {
global $wgContLang;
$spaces = "<namespaces>\n";
foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) {
- $spaces .= ' ' . Xml::element( 'namespace', array( 'key' => $ns ), $title ) . "\n";
+ $spaces .= ' ' .
+ Xml::element( 'namespace',
+ array( 'key' => $ns,
+ 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
+ ), $title ) . "\n";
}
$spaces .= " </namespaces>";
return $spaces;
@@ -440,10 +446,16 @@ class XmlDumpWriter {
$title = Title::makeTitle( $row->page_namespace, $row->page_title );
$out .= ' ' . Xml::elementClean( 'title', array(), $title->getPrefixedText() ) . "\n";
$out .= ' ' . Xml::element( 'id', array(), strval( $row->page_id ) ) . "\n";
- if( '' != $row->page_restrictions ) {
+ if( $row->page_is_redirect ) {
+ $out .= ' ' . Xml::element( 'redirect', array() ) . "\n";
+ }
+ if( $row->page_restrictions != '' ) {
$out .= ' ' . Xml::element( 'restrictions', array(),
strval( $row->page_restrictions ) ) . "\n";
}
+
+ wfRunHooks( 'XmlDumpWriterOpenPage', array( $this, &$out, $row, $title ) );
+
return $out;
}
@@ -488,6 +500,7 @@ class XmlDumpWriter {
$out .= " " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
}
+ $text = '';
if( $row->rev_deleted & Revision::DELETED_TEXT ) {
$out .= " " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
} elseif( isset( $row->old_text ) ) {
@@ -503,6 +516,8 @@ class XmlDumpWriter {
"" ) . "\n";
}
+ wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
+
$out .= " </revision>\n";
wfProfileOut( $fname );
diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php
index 1e750bb5..6a779079 100644
--- a/includes/ExternalStore.php
+++ b/includes/ExternalStore.php
@@ -13,8 +13,20 @@
* @ingroup ExternalStorage
*/
class ExternalStore {
- /* Fetch data from given URL */
- static function fetchFromURL( $url ) {
+ var $mParams;
+
+ function __construct( $params = array() ) {
+ $this->mParams = $params;
+ }
+
+ /**
+ * Fetch data from given URL
+ *
+ * @param $url String: The URL of the text to get
+ * @param $params Array: associative array of parameters for the ExternalStore object.
+ * @return The text stored or false on error
+ */
+ static function fetchFromURL( $url, $params = array() ) {
global $wgExternalStores;
if( !$wgExternalStores )
@@ -25,16 +37,20 @@ class ExternalStore {
if( $path == '' )
return false;
- $store = self::getStoreObject( $proto );
+ $store = self::getStoreObject( $proto, $params );
if ( $store === false )
return false;
return $store->fetchFromURL( $url );
}
/**
- * Get an external store object of the given type
+ * Get an external store object of the given type, with the given parameters
+ *
+ * @param $proto String: type of external storage, should be a value in $wgExternalStores
+ * @param $params Array: associative array of parameters for the ExternalStore object.
+ * @return ExternalStore subclass or false on error
*/
- static function getStoreObject( $proto ) {
+ static function getStoreObject( $proto, $params = array() ) {
global $wgExternalStores;
if( !$wgExternalStores )
return false;
@@ -48,18 +64,18 @@ class ExternalStore {
return false;
}
- return new $class();
+ return new $class($params);
}
/**
* Store a data item to an external store, identified by a partial URL
* The protocol part is used to identify the class, the rest is passed to the
* class itself as a parameter.
- * Returns the URL of the stored data item, or false on error
+ * @return The URL of the stored data item, or false on error
*/
- static function insert( $url, $data ) {
+ static function insert( $url, $data, $params = array() ) {
list( $proto, $params ) = explode( '://', $url, 2 );
- $store = self::getStoreObject( $proto );
+ $store = self::getStoreObject( $proto, $params );
if ( $store === false ) {
return false;
} else {
@@ -72,10 +88,11 @@ class ExternalStore {
* This function does not need a url param, it builds it by
* itself. It also fails-over to the next possible clusters.
*
- * @param string $data
- * Returns the URL of the stored data item, or false on error
+ * @param $data String
+ * @param $storageParams Array: associative array of parameters for the ExternalStore object.
+ * @return The URL of the stored data item, or false on error
*/
- public static function insertToDefault( $data ) {
+ public static function insertToDefault( $data, $storageParams = array() ) {
global $wgDefaultExternalStore;
$tryStores = (array)$wgDefaultExternalStore;
$error = false;
@@ -84,7 +101,7 @@ class ExternalStore {
$storeUrl = $tryStores[$index];
wfDebug( __METHOD__.": trying $storeUrl\n" );
list( $proto, $params ) = explode( '://', $storeUrl, 2 );
- $store = self::getStoreObject( $proto );
+ $store = self::getStoreObject( $proto, $storageParams );
if ( $store === false ) {
throw new MWException( "Invalid external storage protocol - $storeUrl" );
}
@@ -111,4 +128,9 @@ class ExternalStore {
throw new MWException( "Unable to store text to external storage" );
}
}
+
+ /** Like insertToDefault, but inserts on another wiki */
+ public static function insertToForeignDefault( $data, $wiki ) {
+ return self::insertToDefault( $data, array( 'wiki' => $wiki ) );
+ }
}
diff --git a/includes/ExternalStoreDB.php b/includes/ExternalStoreDB.php
index 9fa7d1b1..769c64da 100644
--- a/includes/ExternalStoreDB.php
+++ b/includes/ExternalStoreDB.php
@@ -26,24 +26,52 @@ $wgExternalBlobCache = array();
*/
class ExternalStoreDB {
- /** @todo Document.*/
+ function __construct( $params = array() ) {
+ $this->mParams = $params;
+ }
+
+ /**
+ * Get a LoadBalancer for the specified cluster
+ *
+ * @param $cluster String: cluster name
+ * @return LoadBalancer object
+ */
function &getLoadBalancer( $cluster ) {
- return wfGetLBFactory()->getExternalLB( $cluster );
+ $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false;
+
+ return wfGetLBFactory()->getExternalLB( $cluster, $wiki );
}
- /** @todo Document.*/
+ /**
+ * Get a slave database connection for the specified cluster
+ *
+ * @param $cluster String: cluster name
+ * @return DatabaseBase object
+ */
function &getSlave( $cluster ) {
+ $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false;
$lb =& $this->getLoadBalancer( $cluster );
- return $lb->getConnection( DB_SLAVE );
+ return $lb->getConnection( DB_SLAVE, array(), $wiki );
}
- /** @todo Document.*/
+ /**
+ * Get a master database connection for the specified cluster
+ *
+ * @param $cluster String: cluster name
+ * @return DatabaseBase object
+ */
function &getMaster( $cluster ) {
+ $wiki = isset($this->mParams['wiki']) ? $this->mParams['wiki'] : false;
$lb =& $this->getLoadBalancer( $cluster );
- return $lb->getConnection( DB_MASTER );
+ return $lb->getConnection( DB_MASTER, array(), $wiki );
}
- /** @todo Document.*/
+ /**
+ * Get the 'blobs' table name for this database
+ *
+ * @param $db DatabaseBase
+ * @return String: table name ('blobs' by default)
+ */
function getTable( &$db ) {
$table = $db->getLBInfo( 'blobs table' );
if ( is_null( $table ) ) {
@@ -54,7 +82,7 @@ class ExternalStoreDB {
/**
* Fetch data from given URL
- * @param string $url An url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage.
+ * @param $url String: an url of the form DB://cluster/id or DB://cluster/id/itemid for concatened storage.
*/
function fetchFromURL( $url ) {
$path = explode( '/', $url );
@@ -128,8 +156,11 @@ class ExternalStoreDB {
array( 'blob_id' => $id, 'blob_text' => $data ),
__METHOD__ );
$id = $dbw->insertId();
+ if ( !$id ) {
+ throw new MWException( __METHOD__.': no insert ID' );
+ }
if ( $dbw->getFlag( DBO_TRX ) ) {
- $dbw->immediateCommit();
+ $dbw->commit();
}
return "DB://$cluster/$id";
}
diff --git a/includes/ExternalStoreHttp.php b/includes/ExternalStoreHttp.php
index 6eb33b39..092ff7d8 100644
--- a/includes/ExternalStoreHttp.php
+++ b/includes/ExternalStoreHttp.php
@@ -1,15 +1,21 @@
<?php
+
/**
- * Example class for HTTP accessable external objects
+ * Example class for HTTP accessable external objects.
+ * Only supports reading, not storing.
*
* @ingroup ExternalStorage
*/
class ExternalStoreHttp {
- /* Fetch data from given URL */
- function fetchFromURL($url) {
- ini_set( "allow_url_fopen", true );
- $ret = file_get_contents( $url );
- ini_set( "allow_url_fopen", false );
+
+ /**
+ * Fetch data from given URL
+ *
+ * @param $url String: the URL
+ * @return String: the content at $url
+ */
+ function fetchFromURL( $url ) {
+ $ret = Http::get( $url );
return $ret;
}
diff --git a/includes/ExternalUser.php b/includes/ExternalUser.php
new file mode 100644
index 00000000..65dae617
--- /dev/null
+++ b/includes/ExternalUser.php
@@ -0,0 +1,304 @@
+<?php
+
+# Copyright (C) 2009 Aryeh Gregor
+#
+# 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
+
+/**
+ * @defgroup ExternalUser ExternalUser
+ */
+
+/**
+ * A class intended to supplement, and perhaps eventually replace, AuthPlugin.
+ * See: http://www.mediawiki.org/wiki/ExternalAuth
+ *
+ * The class represents a user whose data is in a foreign database. The
+ * database may have entirely different conventions from MediaWiki, but it's
+ * assumed to at least support the concept of a user id (possibly not an
+ * integer), a user name (possibly not meeting MediaWiki's username
+ * requirements), and a password.
+ *
+ * @ingroup ExternalUser
+ */
+abstract class ExternalUser {
+ protected function __construct() {}
+
+ /**
+ * Wrappers around initFrom*().
+ */
+
+ /**
+ * @param $name string
+ * @return mixed ExternalUser, or false on failure
+ */
+ public static function newFromName( $name ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ return false;
+ }
+ $obj = new $wgExternalAuthType;
+ if ( !$obj->initFromName( $name ) ) {
+ return false;
+ }
+ return $obj;
+ }
+
+ /**
+ * @param $id string
+ * @return mixed ExternalUser, or false on failure
+ */
+ public static function newFromId( $id ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ return false;
+ }
+ $obj = new $wgExternalAuthType;
+ if ( !$obj->initFromId( $id ) ) {
+ return false;
+ }
+ return $obj;
+ }
+
+ /**
+ * @return mixed ExternalUser, or false on failure
+ */
+ public static function newFromCookie() {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ return false;
+ }
+ $obj = new $wgExternalAuthType;
+ if ( !$obj->initFromCookie() ) {
+ return false;
+ }
+ return $obj;
+ }
+
+ /**
+ * Creates the object corresponding to the given User object, assuming the
+ * user exists on the wiki and is linked to an external account. If either
+ * of these is false, this will return false.
+ *
+ * This is a wrapper around newFromId().
+ *
+ * @param $user User
+ * @return mixed ExternalUser or false
+ */
+ public static function newFromUser( $user ) {
+ global $wgExternalAuthType;
+ if ( is_null( $wgExternalAuthType ) ) {
+ # Short-circuit to avoid database query in common case so no one
+ # kills me
+ return false;
+ }
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $id = $dbr->selectField( 'external_user', 'eu_external_id',
+ array( 'eu_local_id' => $user->getId() ), __METHOD__ );
+ if ( $id === false ) {
+ return false;
+ }
+ return self::newFromId( $id );
+ }
+
+ /**
+ * Given a name, which is a string exactly as input by the user in the
+ * login form but with whitespace stripped, initialize this object to be
+ * the corresponding ExternalUser. Return true if successful, otherwise
+ * false.
+ *
+ * @param $name string
+ * @return bool Success?
+ */
+ protected abstract function initFromName( $name );
+
+ /**
+ * Given an id, which was at some previous point in history returned by
+ * getId(), initialize this object to be the corresponding ExternalUser.
+ * Return true if successful, false otherwise.
+ *
+ * @param $id string
+ * @return bool Success?
+ */
+ protected abstract function initFromId( $id );
+
+ /**
+ * Try to magically initialize the user from cookies or similar information
+ * so he or she can be logged in on just viewing the wiki. If this is
+ * impossible to do, just return false.
+ *
+ * TODO: Actually use this.
+ *
+ * @return bool Success?
+ */
+ protected function initFromCookie() {
+ return false;
+ }
+
+ /**
+ * This must return some identifier that stably, uniquely identifies the
+ * user. In a typical web application, this could be an integer
+ * representing the "user id". In other cases, it might be a string. In
+ * any event, the return value should be a string between 1 and 255
+ * characters in length; must uniquely identify the user in the foreign
+ * database; and, if at all possible, should be permanent.
+ *
+ * This will only ever be used to reconstruct this ExternalUser object via
+ * newFromId(). The resulting object in that case should correspond to the
+ * same user, even if details have changed in the interim (e.g., renames or
+ * preference changes).
+ *
+ * @return string
+ */
+ abstract public function getId();
+
+ /**
+ * This must return the name that the user would normally use for login to
+ * the external database. It is subject to no particular restrictions
+ * beyond rudimentary sanity, and in particular may be invalid as a
+ * MediaWiki username. It's used to auto-generate an account name that
+ * *is* valid for MediaWiki, either with or without user input, but
+ * basically is only a hint.
+ *
+ * @return string
+ */
+ abstract public function getName();
+
+ /**
+ * Is the given password valid for the external user? The password is
+ * provided in plaintext.
+ *
+ * @param $password string
+ * @return bool
+ */
+ abstract public function authenticate( $password );
+
+ /**
+ * Retrieve the value corresponding to the given preference key. The most
+ * important values are:
+ *
+ * - emailaddress
+ * - language
+ *
+ * The value must meet MediaWiki's requirements for values of this type,
+ * and will be checked for validity before use. If the preference makes no
+ * sense for the backend, or it makes sense but is unset for this user, or
+ * is unrecognized, return null.
+ *
+ * $pref will never equal 'password', since passwords are usually hashed
+ * and cannot be directly retrieved. authenticate() is used for this
+ * instead.
+ *
+ * TODO: Currently this is only called for 'emailaddress'; generalize! Add
+ * some config option to decide which values are grabbed on user
+ * initialization.
+ *
+ * @param $pref string
+ * @return mixed
+ */
+ public function getPref( $pref ) {
+ return null;
+ }
+
+ /**
+ * Return an array of identifiers for all the foreign groups that this user
+ * has. The identifiers are opaque objects that only need to be
+ * specifiable by the administrator in LocalSettings.php when configuring
+ * $wgAutopromote. They may be, for instance, strings or integers.
+ *
+ * TODO: Support this in $wgAutopromote.
+ *
+ * @return array
+ */
+ public function getGroups() {
+ return array();
+ }
+
+ /**
+ * Given a preference key (e.g., 'emailaddress'), provide an HTML message
+ * telling the user how to change it in the external database. The
+ * administrator has specified that this preference cannot be changed on
+ * the wiki, and may only be changed in the foreign database. If no
+ * message is available, such as for an unrecognized preference, return
+ * false.
+ *
+ * TODO: Use this somewhere.
+ *
+ * @param $pref string
+ * @return mixed String or false
+ */
+ public static function getPrefMessage( $pref ) {
+ return false;
+ }
+
+ /**
+ * Set the given preference key to the given value. Two important
+ * preference keys that you might want to implement are 'password' and
+ * 'emailaddress'. If the set fails, such as because the preference is
+ * unrecognized or because the external database can't be changed right
+ * now, return false. If it succeeds, return true.
+ *
+ * If applicable, you should make sure to validate the new value against
+ * any constraints the external database may have, since MediaWiki may have
+ * more limited constraints (e.g., on password strength).
+ *
+ * TODO: Untested.
+ *
+ * @param $key string
+ * @param $value string
+ * @return bool Success?
+ */
+ public static function setPref( $key, $value ) {
+ return false;
+ }
+
+ /**
+ * Create a link for future reference between this object and the provided
+ * user_id. If the user was already linked, the old link will be
+ * overwritten.
+ *
+ * This is part of the core code and is not overridable by specific
+ * plugins. It's in this class only for convenience.
+ *
+ * @param $id int user_id
+ */
+ public final function linkToLocal( $id ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'external_user',
+ array( 'eu_local_id', 'eu_external_id' ),
+ array( 'eu_local_id' => $id,
+ 'eu_external_id' => $this->getId() ),
+ __METHOD__ );
+ }
+
+ /**
+ * Check whether this external user id is already linked with
+ * a local user.
+ * @return Mixed User if the account is linked, Null otherwise.
+ */
+ public final function getLocalUser(){
+ $dbr = wfGetDb( DB_SLAVE );
+ $row = $dbr->selectRow(
+ 'external_user',
+ '*',
+ array( 'eu_external_id' => $this->getId() )
+ );
+ return $row
+ ? User::newFromId( $row->eu_local_id )
+ : null;
+ }
+
+}
diff --git a/includes/FakeTitle.php b/includes/FakeTitle.php
index 10bfa538..21b49bde 100644
--- a/includes/FakeTitle.php
+++ b/includes/FakeTitle.php
@@ -17,17 +17,25 @@ class FakeTitle extends Title {
function getDBkey() { $this->error(); }
function getNamespace() { $this->error(); }
function getNsText() { $this->error(); }
+ function getUserCaseDBKey() { $this->error(); }
function getSubjectNsText() { $this->error(); }
+ function getTalkNsText() { $this->error(); }
+ function canTalk() { $this->error(); }
function getInterwiki() { $this->error(); }
function getFragment() { $this->error(); }
+ function getFragmentForURL() { $this->error(); }
function getDefaultNamespace() { $this->error(); }
function getIndexTitle() { $this->error(); }
function getPrefixedDBkey() { $this->error(); }
function getPrefixedText() { $this->error(); }
function getFullText() { $this->error(); }
+ function getBaseText() { $this->error(); }
+ function getSubpageText() { $this->error(); }
+ function getSubpageUrlForm() { $this->error(); }
function getPrefixedURL() { $this->error(); }
function getFullURL( $query = '', $variant = false ) {$this->error(); }
function getLocalURL( $query = '', $variant = false ) { $this->error(); }
+ function getLinkUrl( $query = array(), $variant = false ) { $this->error(); }
function escapeLocalURL( $query = '' ) { $this->error(); }
function escapeFullURL( $query = '' ) { $this->error(); }
function getInternalURL( $query = '', $variant = false ) { $this->error(); }
@@ -36,49 +44,88 @@ class FakeTitle extends Title {
function isExternal() { $this->error(); }
function isSemiProtected( $action = 'edit' ) { $this->error(); }
function isProtected( $action = '' ) { $this->error(); }
+ function isConversionTable() { $this->error(); }
function userIsWatching() { $this->error(); }
+ function quickUserCan( $action ) { $this->error(); }
+ function isNamespaceProtected() { $this->error(); }
function userCan( $action, $doExpensiveQueries = true ) { $this->error(); }
- function userCanCreate() { $this->error(); }
- function userCanEdit( $doExpensiveQueries = true ) { $this->error(); }
- function userCanMove() { $this->error(); }
+ function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true, $ignoreErrors = array() ) { $this->error(); }
+ function updateTitleProtection( $create_perm, $reason, $expiry ) { $this->error(); }
+ function deleteTitleProtection() { $this->error(); }
function isMovable() { $this->error(); }
function userCanRead() { $this->error(); }
function isTalkPage() { $this->error(); }
+ function isSubpage() { $this->error(); }
+ function hasSubpages() { $this->error(); }
+ function getSubpages( $limit = -1 ) { $this->error(); }
function isCssJsSubpage() { $this->error(); }
+ function isCssOrJsPage() { $this->error(); }
function isValidCssJsSubpage() { $this->error(); }
function getSkinFromCssJsSubpage() { $this->error(); }
function isCssSubpage() { $this->error(); }
function isJsSubpage() { $this->error(); }
function userCanEditCssJsSubpage() { $this->error(); }
- function loadRestrictions( $res ) { $this->error(); }
- function getRestrictions($action) { $this->error(); }
+ function userCanEditCssSubpage() { $this->error(); }
+ function userCanEditJsSubpage() { $this->error(); }
+ function isCascadeProtected() { $this->error(); }
+ function getCascadeProtectionSources( $get_pages = true ) { $this->error(); }
+ function areRestrictionsCascading() { $this->error(); }
+ function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { $this->error(); }
+ function loadRestrictions( $res = null ) { $this->error(); }
+ function getRestrictions( $action ) { $this->error(); }
+ function getRestrictionExpiry( $action ) { $this->error(); }
function isDeleted() { $this->error(); }
+ function isDeletedQuick() { $this->error(); }
function getArticleID( $flags = 0 ) { $this->error(); }
- function getLatestRevID() { $this->error(); }
+ function isRedirect( $flags = 0 ) { $this->error(); }
+ function getLength( $flags = 0 ) { $this->error(); }
+ function getLatestRevID( $flags = 0 ) { $this->error(); }
function resetArticleID( $newid ) { $this->error(); }
function invalidateCache() { $this->error(); }
function getTalkPage() { $this->error(); }
+ function setFragment( $fragment ) { $this->error(); }
function getSubjectPage() { $this->error(); }
- function getLinksTo() { $this->error(); }
- function getTemplateLinksTo() { $this->error(); }
+ function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { $this->error(); }
+ function getTemplateLinksTo( $options = array() ) { $this->error(); }
function getBrokenLinksFrom() { $this->error(); }
function getSquidURLs() { $this->error(); }
- function moveNoAuth() { $this->error(); }
- function isValidMoveOperation() { $this->error(); }
- function moveTo() { $this->error(); }
- function moveOverExistingRedirect() { $this->error(); }
- function moveToNewTitle() { $this->error(); }
- function isValidMoveTarget() { $this->error(); }
+ function purgeSquid() { $this->error(); }
+ function moveNoAuth( &$nt ) { $this->error(); }
+ function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) { $this->error(); }
+ function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); }
+ function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { $this->error(); }
+ function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) { $this->error(); }
+ function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { $this->error(); }
+ function isSingleRevRedirect() { $this->error(); }
+ function isValidMoveTarget( $nt ) { $this->error(); }
+ function isWatchable() { $this->error(); }
function getParentCategories() { $this->error(); }
- function getParentCategoryTree() { $this->error(); }
+ function getParentCategoryTree( $children = array() ) { $this->error(); }
function pageCond() { $this->error(); }
- function getPreviousRevisionID() { $this->error(); }
- function getNextRevisionID() { $this->error(); }
- function equals() { $this->error(); }
+ function getPreviousRevisionID( $revId, $flags=0 ) { $this->error(); }
+ function getNextRevisionID( $revId, $flags=0 ) { $this->error(); }
+ function getFirstRevision( $flags=0 ) { $this->error(); }
+ function isNewPage() { $this->error(); }
+ function getEarliestRevTime() { $this->error(); }
+ function countRevisionsBetween( $old, $new ) { $this->error(); }
+ function equals( Title $title ) { $this->error(); }
function exists() { $this->error(); }
function isAlwaysKnown() { $this->error(); }
function isKnown() { $this->error(); }
+ function canExist() { $this->error(); }
function touchLinks() { $this->error(); }
+ function getTouched( $db = null ) { $this->error(); }
+ function getNotificationTimestamp( $user = null ) { $this->error(); }
function trackbackURL() { $this->error(); }
function trackbackRDF() { $this->error(); }
+ function getNamespaceKey( $prepend = 'nstab-' ) { $this->error(); }
+ function isSpecialPage() { $this->error(); }
+ function isSpecial( $name ) { $this->error(); }
+ function fixSpecialName() { $this->error(); }
+ function isContentPage() { $this->error(); }
+ function getRedirectsHere( $ns = null ) { $this->error(); }
+ function isValidRedirectTarget() { $this->error(); }
+ function getBacklinkCache() { $this->error(); }
+ function canUseNoindex() { $this->error(); }
+ function getRestrictionTypes() { $this->error(); }
}
diff --git a/includes/Feed.php b/includes/Feed.php
index fe6d8feb..782b6428 100644
--- a/includes/Feed.php
+++ b/includes/Feed.php
@@ -19,13 +19,19 @@
# http://www.gnu.org/copyleft/gpl.html
/**
+ * @defgroup Feed Feed
+ *
* Basic support for outputting syndication feeds in RSS, other formats.
* Contain a feed class as well as classes to build rss / atom ... feeds
* Available feeds are defined in Defines.php
+ *
+ * @file
*/
/**
* A base class for basic support for outputting syndication feeds in RSS and other formats.
+ *
+ * @ingroup Feed
*/
class FeedItem {
/**#@+
@@ -37,56 +43,134 @@ class FeedItem {
var $Url = '';
var $Date = '';
var $Author = '';
+ var $UniqueId = '';
+ var $RSSIsPermalink;
/**#@-*/
- /**#@+
- * @todo document
- * @param $Url URL uniquely designating the item.
+ /**
+ * Constructor
+ *
+ * @param $Title String: Item's title
+ * @param $Description String
+ * @param $Url String: URL uniquely designating the item.
+ * @param $Date String: Item's date
+ * @param $Author String: Author's user name
+ * @param $Comments String
*/
function __construct( $Title, $Description, $Url, $Date = '', $Author = '', $Comments = '' ) {
$this->Title = $Title;
$this->Description = $Description;
$this->Url = $Url;
+ $this->UniqueId = $Url;
+ $this->RSSIsPermalink = false;
$this->Date = $Date;
$this->Author = $Author;
$this->Comments = $Comments;
}
+ /**
+ * Encode $string so that it can be safely embedded in a XML document
+ *
+ * @param $string String: string to encode
+ * @return String
+ */
public function xmlEncode( $string ) {
$string = str_replace( "\r\n", "\n", $string );
$string = preg_replace( '/[\x00-\x08\x0b\x0c\x0e-\x1f]/', '', $string );
return htmlspecialchars( $string );
}
+ /**
+ * Get the unique id of this item
+ *
+ * @return String
+ */
+ public function getUniqueId() {
+ if ( $this->UniqueId ) {
+ return $this->xmlEncode( $this->UniqueId );
+ }
+ }
+
+ /**
+ * set the unique id of an item
+ *
+ * @param $uniqueId String: unique id for the item
+ * @param $RSSisPermalink Boolean: set to true if the guid (unique id) is a permalink (RSS feeds only)
+ */
+ public function setUniqueId($uniqueId, $RSSisPermalink = False) {
+ $this->UniqueId = $uniqueId;
+ $this->RSSIsPermalink = $isPermalink;
+ }
+
+ /**
+ * Get the title of this item; already xml-encoded
+ *
+ * @return String
+ */
public function getTitle() {
return $this->xmlEncode( $this->Title );
}
+ /**
+ * Get the URL of this item; already xml-encoded
+ *
+ * @return String
+ */
public function getUrl() {
return $this->xmlEncode( $this->Url );
}
+ /**
+ * Get the description of this item; already xml-encoded
+ *
+ * @return String
+ */
public function getDescription() {
return $this->xmlEncode( $this->Description );
}
+ /**
+ * Get the language of this item
+ *
+ * @return String
+ */
public function getLanguage() {
global $wgContLanguageCode;
return $wgContLanguageCode;
}
+ /**
+ * Get the title of this item
+ *
+ * @return String
+ */
public function getDate() {
return $this->Date;
}
+
+ /**
+ * Get the author of this item; already xml-encoded
+ *
+ * @return String
+ */
public function getAuthor() {
return $this->xmlEncode( $this->Author );
}
+
+ /**
+ * Get the comment of this item; already xml-encoded
+ *
+ * @return String
+ */
public function getComments() {
return $this->xmlEncode( $this->Comments );
}
-
+
/**
* Quickie hack... strip out wikilinks to more legible form from the comment.
+ *
+ * @param $text String: wikitext
+ * @return String
*/
public static function stripComment( $text ) {
return preg_replace( '/\[\[([^]]*\|)?([^]]+)\]\]/', '\2', $text );
@@ -96,6 +180,7 @@ class FeedItem {
/**
* @todo document (needs one-sentence top-level class description).
+ * @ingroup Feed
*/
class ChannelFeed extends FeedItem {
/**#@+
@@ -133,10 +218,8 @@ class ChannelFeed extends FeedItem {
*
* This should be called from the outHeader() method,
* but can also be called separately.
- *
- * @public
*/
- function httpHeaders() {
+ public function httpHeaders() {
global $wgOut;
# We take over from $wgOut, excepting its cache header info
@@ -178,13 +261,16 @@ class ChannelFeed extends FeedItem {
/**
* Generate a RSS feed
+ *
+ * @ingroup Feed
*/
class RSSFeed extends ChannelFeed {
/**
* Format a date given a timestamp
- * @param integer $ts Timestamp
- * @return string Date string
+ *
+ * @param $ts Integer: timestamp
+ * @return String: date string
*/
function formatTime( $ts ) {
return gmdate( 'D, d M Y H:i:s \G\M\T', wfTimestamp( TS_UNIX, $ts ) );
@@ -210,13 +296,14 @@ class RSSFeed extends ChannelFeed {
/**
* Output an RSS 2.0 item
- * @param FeedItem item to be output
+ * @param $item FeedItem: item to be output
*/
function outItem( $item ) {
?>
<item>
<title><?php print $item->getTitle() ?></title>
<link><?php print $item->getUrl() ?></link>
+ <guid<?php if( $item->RSSIsPermalink ) print ' isPermaLink="true"' ?>><?php print $item->getUniqueId() ?></guid>
<description><?php print $item->getDescription() ?></description>
<?php if( $item->getDate() ) { ?><pubDate><?php print $this->formatTime( $item->getDate() ) ?></pubDate><?php } ?>
<?php if( $item->getAuthor() ) { ?><dc:creator><?php print $item->getAuthor() ?></dc:creator><?php }?>
@@ -237,6 +324,8 @@ class RSSFeed extends ChannelFeed {
/**
* Generate an Atom feed
+ *
+ * @ingroup Feed
*/
class AtomFeed extends ChannelFeed {
/**
@@ -297,7 +386,7 @@ class AtomFeed extends ChannelFeed {
global $wgMimeType;
?>
<entry>
- <id><?php print $item->getUrl() ?></id>
+ <id><?php print $item->getUniqueId() ?></id>
<title><?php print $item->getTitle() ?></title>
<link rel="alternate" type="<?php print $wgMimeType ?>" href="<?php print $item->getUrl() ?>"/>
<?php if( $item->getDate() ) { ?>
diff --git a/includes/FeedUtils.php b/includes/FeedUtils.php
index 38bff363..7e841f32 100644
--- a/includes/FeedUtils.php
+++ b/includes/FeedUtils.php
@@ -1,8 +1,20 @@
<?php
-// TODO: document
+/**
+ * Helper functions for feeds
+ *
+ * @ingroup Feed
+ */
class FeedUtils {
+ /**
+ * Check whether feed's cache should be cleared; for changes feeds
+ * If the feed should be purged; $timekey and $key will be removed from
+ * $messageMemc
+ *
+ * @param $timekey String: cache key of the timestamp of the last item
+ * @param $key String: cache key of feed's content
+ */
public static function checkPurge( $timekey, $key ) {
global $wgRequest, $wgUser, $messageMemc;
$purge = $wgRequest->getVal( 'action' ) === 'purge';
@@ -12,8 +24,14 @@ class FeedUtils {
}
}
+ /**
+ * Check whether feeds can be used and that $type is a valid feed type
+ *
+ * @param $type String: feed type, as requested by the user
+ * @return Boolean
+ */
public static function checkFeedOutput( $type ) {
- global $wgFeed, $wgOut, $wgFeedClasses;
+ global $wgFeed, $wgFeedClasses;
if ( !$wgFeed ) {
global $wgOut;
@@ -30,8 +48,11 @@ class FeedUtils {
}
/**
- * Format a diff for the newsfeed
- */
+ * Format a diff for the newsfeed
+ *
+ * @param $row Object: row from the recentchanges table
+ * @return String
+ */
public static function formatDiff( $row ) {
global $wgUser;
@@ -53,9 +74,20 @@ class FeedUtils {
$actiontext );
}
+ /**
+ * Really format a diff for the newsfeed
+ *
+ * @param $title Title object
+ * @param $oldid Integer: old revision's id
+ * @param $newid Integer: new revision's id
+ * @param $timestamp Integer: new revision's timestamp
+ * @param $comment String: new revision's comment
+ * @param $actiontext String: text of the action; in case of log event
+ * @return String
+ */
public static function formatDiffRow( $title, $oldid, $newid, $timestamp, $comment, $actiontext='' ) {
global $wgFeedDiffCutoff, $wgContLang, $wgUser;
- wfProfileIn( __FUNCTION__ );
+ wfProfileIn( __METHOD__ );
$skin = $wgUser->getSkin();
# log enties
@@ -71,12 +103,14 @@ class FeedUtils {
$anon = new User();
$accErrors = $title->getUserPermissionsErrors( 'read', $anon, true );
- if( $title->getNamespace() >= 0 && !$accErrors ) {
+ if( $title->getNamespace() >= 0 && !$accErrors && $newid ) {
if( $oldid ) {
- wfProfileIn( __FUNCTION__."-dodiff" );
+ wfProfileIn( __METHOD__."-dodiff" );
#$diffText = $de->getDiff( wfMsg( 'revisionasof',
- # $wgContLang->timeanddate( $timestamp ) ),
+ # $wgContLang->timeanddate( $timestamp ),
+ # $wgContLang->date( $timestamp ),
+ # $wgContLang->time( $timestamp ) ),
# wfMsg( 'currentrev' ) );
// Don't bother generating the diff if we won't be able to show it
@@ -85,7 +119,9 @@ class FeedUtils {
$diffText = $de->getDiff(
wfMsg( 'previousrevision' ), // hack
wfMsg( 'revisionasof',
- $wgContLang->timeanddate( $timestamp ) ) );
+ $wgContLang->timeanddate( $timestamp ),
+ $wgContLang->date( $timestamp ),
+ $wgContLang->time( $timestamp ) ) );
}
if ( ( strlen( $diffText ) > $wgFeedDiffCutoff ) || ( $wgFeedDiffCutoff <= 0 ) ) {
@@ -106,7 +142,7 @@ class FeedUtils {
$diffText = UtfNormal::cleanUp( $diffText );
$diffText = self::applyDiffStyle( $diffText );
}
- wfProfileOut( __FUNCTION__."-dodiff" );
+ wfProfileOut( __METHOD__."-dodiff" );
} else {
$rev = Revision::newFromId( $newid );
if( is_null( $rev ) ) {
@@ -120,19 +156,19 @@ class FeedUtils {
$completeText .= $diffText;
}
- wfProfileOut( __FUNCTION__ );
+ wfProfileOut( __METHOD__ );
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
- */
+ * 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: diff's HTML output
+ * @return String: modified HTML
+ * @private
+ */
public static function applyDiffStyle( $text ) {
$styles = array(
'diff' => 'background-color: white; color:black;',
@@ -152,4 +188,4 @@ class FeedUtils {
return $text;
}
-} \ No newline at end of file
+}
diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php
index 5177d35f..dad19524 100644
--- a/includes/FileDeleteForm.php
+++ b/includes/FileDeleteForm.php
@@ -17,7 +17,7 @@ class FileDeleteForm {
/**
* Constructor
*
- * @param File $file File we're deleting
+ * @param $file File object we're deleting
*/
public function __construct( $file ) {
$this->title = $file->getTitle();
@@ -90,7 +90,17 @@ class FileDeleteForm {
$this->showLogEntries();
}
+ /**
+ * Really delete the file
+ *
+ * @param $title Title object
+ * @param $file File object
+ * @param $oldimage String: archive name
+ * @param $reason String: reason of the deletion
+ * @param $suppress Boolean: whether to mark all deleted versions as restricted
+ */
public static function doDelete( &$title, &$file, &$oldimage, $reason, $suppress ) {
+ global $wgUser;
$article = null;
if( $oldimage ) {
$status = $file->deleteOld( $oldimage, $reason, $suppress );
@@ -99,7 +109,7 @@ class FileDeleteForm {
$log = new LogPage( 'delete' );
$logComment = wfMsgForContent( 'deletedrevision', $oldimage );
if( trim( $reason ) != '' )
- $logComment .= ": {$reason}";
+ $logComment .= wfMsgForContent( 'colon-separator' ) . $reason;
$log->addEntry( 'delete', $title, $logComment );
}
} else {
@@ -112,7 +122,7 @@ class FileDeleteForm {
if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason, &$error)) ) {
if( $article->doDeleteArticle( $reason, $suppress, $id ) ) {
global $wgRequest;
- if( $wgRequest->getCheck( 'wpWatch' ) ) {
+ if( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) {
$article->doWatch();
} elseif( $title->userIsWatching() ) {
$article->doUnwatch();
@@ -137,10 +147,10 @@ class FileDeleteForm {
if( $wgUser->isAllowed( 'suppressrevision' ) ) {
$suppress = "<tr id=\"wpDeleteSuppressRow\">
<td></td>
- <td class='mw-input'>" .
+ <td class='mw-input'><strong>" .
Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '3' ) ) .
- "</td>
+ "</strong></td>
</tr>";
} else {
$suppress = '';
@@ -173,14 +183,18 @@ class FileDeleteForm {
array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) .
"</td>
</tr>
- {$suppress}
+ {$suppress}";
+ if( $wgUser->isLoggedIn() ) {
+ $form .= "
<tr>
<td></td>
<td class='mw-input'>" .
Xml::checkLabel( wfMsg( 'watchthis' ),
'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
"</td>
- </tr>
+ </tr>";
+ }
+ $form .= "
<tr>
<td></td>
<td class='mw-submit'>" .
@@ -194,7 +208,13 @@ class FileDeleteForm {
if ( $wgUser->isAllowed( 'editinterface' ) ) {
$skin = $wgUser->getSkin();
- $link = $skin->makeLink ( 'MediaWiki:Filedelete-reason-dropdown', wfMsgHtml( 'filedelete-edit-reasonlist' ) );
+ $title = Title::makeTitle( NS_MEDIAWIKI, 'Filedelete-reason-dropdown' );
+ $link = $skin->link(
+ $title,
+ wfMsgHtml( 'filedelete-edit-reasonlist' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
$form .= '<p class="mw-filedelete-editreasons">' . $link . '</p>';
}
@@ -215,8 +235,8 @@ class FileDeleteForm {
* showing an appropriate message depending upon whether
* it's a current file or an old version
*
- * @param string $message Message base
- * @return string
+ * @param $message String: message base
+ * @return String
*/
private function prepareMessage( $message ) {
global $wgLang;
@@ -245,7 +265,16 @@ class FileDeleteForm {
global $wgOut, $wgUser;
$wgOut->setPageTitle( wfMsg( 'filedelete', $this->title->getText() ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->setSubtitle( wfMsg( 'filedelete-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) );
+ $wgOut->setSubtitle( wfMsg(
+ 'filedelete-backlink',
+ $wgUser->getSkin()->link(
+ $this->title,
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ )
+ ) );
}
/**
@@ -279,10 +308,12 @@ class FileDeleteForm {
*/
private function getAction() {
$q = array();
- $q[] = 'action=delete';
+ $q['action'] = 'delete';
+
if( $this->oldimage )
- $q[] = 'oldimage=' . urlencode( $this->oldimage );
- return $this->title->getLocalUrl( implode( '&', $q ) );
+ $q['oldimage'] = $this->oldimage;
+
+ return $this->title->getLocalUrl( $q );
}
/**
@@ -293,5 +324,4 @@ class FileDeleteForm {
private function getTimestamp() {
return $this->oldfile->getTimestamp();
}
-
}
diff --git a/includes/FileRevertForm.php b/includes/FileRevertForm.php
index c7c73246..eb16693a 100644
--- a/includes/FileRevertForm.php
+++ b/includes/FileRevertForm.php
@@ -17,7 +17,7 @@ class FileRevertForm {
/**
* Constructor
*
- * @param File $file File we're reverting
+ * @param $file File we're reverting
*/
public function __construct( $file ) {
$this->title = $file->getTitle();
@@ -114,7 +114,16 @@ class FileRevertForm {
global $wgOut, $wgUser;
$wgOut->setPageTitle( wfMsg( 'filerevert', $this->title->getText() ) );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->setSubtitle( wfMsg( 'filerevert-backlink', $wgUser->getSkin()->makeKnownLinkObj( $this->title ) ) );
+ $wgOut->setSubtitle( wfMsg(
+ 'filerevert-backlink',
+ $wgUser->getSkin()->link(
+ $this->title,
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ )
+ ) );
}
/**
diff --git a/includes/FileStore.php b/includes/FileStore.php
deleted file mode 100644
index 278777b4..00000000
--- a/includes/FileStore.php
+++ /dev/null
@@ -1,360 +0,0 @@
-<?php
-
-/**
- * @todo document (needs one-sentence top-level class description).
- */
-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,
- $info['directory'],
- $info['url'],
- intval( $info['hash'] ) );
- } else {
- 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
- * suffer an uncaught error the lock will be released when the
- * connection is closed.
- * @see Database::lock()
- */
- static function lock() {
- $dbw = wfGetDB( DB_MASTER );
- $lockname = $dbw->addQuotes( FileStore::lockName() );
- return $dbw->lock( $lockname, __METHOD__ );
- }
-
- /**
- * Release the global file store lock.
- * @see Database::unlock()
- */
- static function unlock() {
- $dbw = wfGetDB( DB_MASTER );
- $lockname = $dbw->addQuotes( FileStore::lockName() );
- return $dbw->unlock( $lockname, __METHOD__ );
- }
-
- 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.
- *
- * @param $key storage key string
- * @param $flags
- * DELETE_ORIGINAL - remove the source file on transaction commit.
- *
- * @throws FSException if copy can't be completed
- * @return FSTransaction
- */
- function insert( $key, $sourcePath, $flags=0 ) {
- $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.
- *
- * @param $key storage key string
- * @param $flags
- * DELETE_ORIGINAL - remove the source file on transaction commit.
- *
- * @throws FSException if copy can't be completed
- * @return FSTransaction on success
- */
- function export( $key, $destPath, $flags=0 ) {
- $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 {
- if( !file_exists( dirname( $destPath ) ) ) {
- wfSuppressWarnings();
- $ok = wfMkdirParents( dirname( $destPath ) );
- 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 );
- } else {
- throw new FSException(
- __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.
- *
- * File is not actually removed until transaction commit.
- * Should be protected by FileStore::lock() to avoid race conditions.
- *
- * @param $key storage key string
- * @throws FSException if file can't be deleted
- * @return FSTransaction
- */
- function delete( $key ) {
- $destPath = $this->filePath( $key );
- if( false === $destPath ) {
- throw new FSException( "file store does not contain file '$key'" );
- } else {
- return FileStore::deleteFile( $destPath );
- }
- }
-
- /**
- * Delete a non-managed file on a transactional basis.
- *
- * File is not actually removed until transaction commit.
- * Should be protected by FileStore::lock() to avoid race conditions.
- *
- * @param $path file to remove
- * @throws FSException if file can't be deleted
- * @return FSTransaction
- *
- * @todo Might be worth preliminary permissions check
- */
- static function deleteFile( $path ) {
- if( file_exists( $path ) ) {
- $transaction = new FSTransaction();
- $transaction->addCommit( FSTransaction::DELETE_FILE, $path );
- return $transaction;
- } else {
- 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.
- * @return true on success, false on failure
- */
- function stream( $key ) {
- $path = $this->filePath( $key );
- if( $path === false ) {
- 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 {
- return wfHttpError( 404, "Not found",
- "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.
- *
- * Key should consist of a 31-digit base-36 SHA-1 hash and
- * an optional alphanumeric extension, all lowercase.
- * The whole must not exceed 64 characters.
- *
- * @param $key
- * @return boolean
- */
- 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
- * out of a temporary file etc.
- *
- * @param $path to file
- * @param $extension
- * @return string or false if could not open file or bad extension
- */
- static function calculateKey( $path, $extension ) {
- wfSuppressWarnings();
- $hash = sha1_file( $path );
- wfRestoreWarnings();
- if( $hash === false ) {
- 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;
- } else {
- wfDebug( __METHOD__.": generated bad key '$key'\n" );
- return false;
- }
- }
-
- /**
- * Return filesystem path to the given file.
- * Note that the file may or may not exist.
- * @return string or false if an invalid key
- */
- function filePath( $key ) {
- if( self::validKey( $key ) ) {
- return $this->mDirectory . DIRECTORY_SEPARATOR .
- $this->hashPath( $key, DIRECTORY_SEPARATOR );
- } else {
- return false;
- }
- }
-
- /**
- * Return URL path to the given file, if the store is public.
- * @return string or false if not public
- */
- function urlPath( $key ) {
- if( $this->mUrl && self::validKey( $key ) ) {
- return $this->mUrl . '/' . $this->hashPath( $key, '/' );
- } else {
- return false;
- }
- }
-
- private function hashPath( $key, $separator ) {
- $parts = array();
- for( $i = 0; $i < $this->mHashLevel; $i++ ) {
- $parts[] = $key{$i};
- }
- $parts[] = $key;
- return implode( $separator, $parts );
- }
-}
-
-/**
- * Wrapper for file store transaction stuff.
- *
- * FileStore methods may return one of these for undoable operations;
- * you can then call its rollback() or commit() methods to perform
- * final cleanup if dependent database work fails or succeeds.
- */
-class FSTransaction {
- const DELETE_FILE = 1;
-
- /**
- * Combine more items into a fancier transaction
- */
- function add( FSTransaction $transaction ) {
- $this->mOnCommit = array_merge(
- $this->mOnCommit, $transaction->mOnCommit );
- $this->mOnRollback = array_merge(
- $this->mOnRollback, $transaction->mOnRollback );
- }
-
- /**
- * Perform final actions for success.
- * @return true if actions applied ok, false if errors
- */
- function commit() {
- return $this->apply( $this->mOnCommit );
- }
-
- /**
- * Perform final actions for failure.
- * @return true if actions applied ok, false if errors
- */
- 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 ) {
- list( $action, $path ) = $item;
- if( $action == self::DELETE_FILE ) {
- wfSuppressWarnings();
- $ok = unlink( $path );
- wfRestoreWarnings();
- if( $ok )
- wfDebug( __METHOD__.": deleting file '$path'\n" );
- else
- wfDebug( __METHOD__.": failed to delete file '$path'\n" );
- $result = $result && $ok;
- }
- }
- return $result;
- }
-}
-
-/**
- * @ingroup Exception
- */
-class FSException extends MWException { }
diff --git a/includes/ForkController.php b/includes/ForkController.php
index 09e1788b..7b889228 100644
--- a/includes/ForkController.php
+++ b/includes/ForkController.php
@@ -2,10 +2,12 @@
/**
* Class for managing forking command line scripts.
- * Currently just does forking and process control, but it could easily be extended
+ * Currently just does forking and process control, but it could easily be extended
* to provide IPC and job dispatch.
*
* This class requires the posix and pcntl extensions.
+ *
+ * @ingroup Maintenance
*/
class ForkController {
var $children = array();
@@ -39,13 +41,13 @@ class ForkController {
}
/**
- * Start the child processes.
+ * Start the child processes.
*
- * This should only be called from the command line. It should be called
+ * This should only be called from the command line. It should be called
* as early as possible during execution.
*
- * This will return 'child' in the child processes. In the parent process,
- * it will run until all the child processes exit or a TERM signal is
+ * This will return 'child' in the child processes. In the parent process,
+ * it will run until all the child processes exit or a TERM signal is
* received. It will then return 'done'.
*/
public function start() {
@@ -73,16 +75,18 @@ class ForkController {
// Restart if the signal was abnormal termination
// Don't restart if it was deliberately killed
$signal = pcntl_wtermsig( $status );
- if ( in_array( $signal, self::$restartableSignals ) ) {
+ if ( in_array( $signal, self::$restartableSignals ) ) {
echo "Worker exited with signal $signal, restarting\n";
$this->procsToStart++;
}
} elseif ( pcntl_wifexited( $status ) ) {
// Restart on non-zero exit status
$exitStatus = pcntl_wexitstatus( $status );
- if ( $exitStatus > 0 ) {
+ if ( $exitStatus != 0 ) {
echo "Worker exited with status $exitStatus, restarting\n";
$this->procsToStart++;
+ } else {
+ echo "Worker exited normally\n";
}
}
}
@@ -96,7 +100,7 @@ class ForkController {
if ( function_exists( 'pcntl_signal_dispatch' ) ) {
pcntl_signal_dispatch();
} else {
- declare (ticks=1) { $status = $status; }
+ declare (ticks=1) { $status = $status; }
}
// Respond to TERM signal
if ( $this->termReceived ) {
@@ -123,7 +127,7 @@ class ForkController {
*/
protected function forkWorkers( $numProcs ) {
global $wgMemc, $wgCaches, $wgMainCacheType;
-
+
$this->prepareEnvironment();
// Create the child processes
@@ -151,7 +155,7 @@ class ForkController {
global $wgMemc, $wgMainCacheType;
$wgMemc = wfGetCache( $wgMainCacheType );
$this->children = null;
- pcntl_signal( SIGTERM, SIG_DFL );
+ pcntl_signal( SIGTERM, SIG_DFL );
}
protected function handleTermSignal( $signal ) {
diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php
index 0807f0be..d6e0f5b4 100644
--- a/includes/GlobalFunctions.php
+++ b/includes/GlobalFunctions.php
@@ -33,18 +33,71 @@ if( !function_exists('iconv') ) {
}
}
-# UTF-8 substr function based on a PHP manual comment
if ( !function_exists( 'mb_substr' ) ) {
- function mb_substr( $str, $start ) {
- $ar = array();
- preg_match_all( '/./us', $str, $ar );
-
- if( func_num_args() >= 3 ) {
- $end = func_get_arg( 2 );
- return join( '', array_slice( $ar[0], $start, $end ) );
+ /**
+ * Fallback implementation for mb_substr, hardcoded to UTF-8.
+ * Attempts to be at least _moderately_ efficient; best optimized
+ * for relatively small offset and count values -- about 5x slower
+ * than native mb_string in my testing.
+ *
+ * Larger offsets are still fairly efficient for Latin text, but
+ * can be up to 100x slower than native if the text is heavily
+ * multibyte and we have to slog through a few hundred kb.
+ */
+ function mb_substr( $str, $start, $count='end' ) {
+ if( $start != 0 ) {
+ $split = mb_substr_split_unicode( $str, intval( $start ) );
+ $str = substr( $str, $split );
+ }
+
+ if( $count !== 'end' ) {
+ $split = mb_substr_split_unicode( $str, intval( $count ) );
+ $str = substr( $str, 0, $split );
+ }
+
+ return $str;
+ }
+
+ function mb_substr_split_unicode( $str, $splitPos ) {
+ if( $splitPos == 0 ) {
+ return 0;
+ }
+
+ $byteLen = strlen( $str );
+
+ if( $splitPos > 0 ) {
+ if( $splitPos > 256 ) {
+ // Optimize large string offsets by skipping ahead N bytes.
+ // This will cut out most of our slow time on Latin-based text,
+ // and 1/2 to 1/3 on East European and Asian scripts.
+ $bytePos = $splitPos;
+ while ($bytePos < $byteLen && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0")
+ ++$bytePos;
+ $charPos = mb_strlen( substr( $str, 0, $bytePos ) );
+ } else {
+ $charPos = 0;
+ $bytePos = 0;
+ }
+
+ while( $charPos++ < $splitPos ) {
+ ++$bytePos;
+ // Move past any tail bytes
+ while ($bytePos < $byteLen && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0")
+ ++$bytePos;
+ }
} else {
- return join( '', array_slice( $ar[0], $start ) );
+ $splitPosX = $splitPos + 1;
+ $charPos = 0; // relative to end of string; we don't care about the actual char position here
+ $bytePos = $byteLen;
+ while( $bytePos > 0 && $charPos-- >= $splitPosX ) {
+ --$bytePos;
+ // Move past any tail bytes
+ while ($bytePos > 0 && $str{$bytePos} >= "\x80" && $str{$bytePos} < "\xc0")
+ --$bytePos;
+ }
}
+
+ return $bytePos;
}
}
@@ -72,6 +125,54 @@ if ( !function_exists( 'mb_strlen' ) ) {
}
}
+
+if( !function_exists( 'mb_strpos' ) ) {
+ /**
+ * Fallback implementation of mb_strpos, hardcoded to UTF-8.
+ * @param $haystack String
+ * @param $needle String
+ * @param $offset String: optional start position
+ * @param $encoding String: optional encoding; ignored
+ * @return int
+ */
+ function mb_strpos( $haystack, $needle, $offset = 0, $encoding="" ) {
+ $needle = preg_quote( $needle, '/' );
+
+ $ar = array();
+ preg_match( '/'.$needle.'/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset );
+
+ if( isset( $ar[0][1] ) ) {
+ return $ar[0][1];
+ } else {
+ return false;
+ }
+ }
+}
+
+if( !function_exists( 'mb_strrpos' ) ) {
+ /**
+ * Fallback implementation of mb_strrpos, hardcoded to UTF-8.
+ * @param $haystack String
+ * @param $needle String
+ * @param $offset String: optional start position
+ * @param $encoding String: optional encoding; ignored
+ * @return int
+ */
+ function mb_strrpos( $haystack, $needle, $offset = 0, $encoding = "" ) {
+ $needle = preg_quote( $needle, '/' );
+
+ $ar = array();
+ preg_match_all( '/'.$needle.'/u', $haystack, $ar, PREG_OFFSET_CAPTURE, $offset );
+
+ if( isset( $ar[0] ) && count( $ar[0] ) > 0 &&
+ isset( $ar[0][count($ar[0])-1][1] ) ) {
+ return $ar[0][count($ar[0])-1][1];
+ } else {
+ return false;
+ }
+ }
+}
+
if ( !function_exists( 'array_diff_key' ) ) {
/**
* Exists in PHP 5.1.0+
@@ -89,6 +190,37 @@ if ( !function_exists( 'array_diff_key' ) ) {
}
}
+if ( !function_exists( 'array_intersect_key' ) ) {
+ /**
+ * Exists in 5.1.0+
+ * Define our own array_intersect_key function
+ */
+ function array_intersect_key( $isec, $keys ) {
+ $argc = func_num_args();
+
+ if ( $argc > 2 ) {
+ for ( $i = 1; $isec && $i < $argc; $i++ ) {
+ $arr = func_get_arg( $i );
+
+ foreach ( array_keys( $isec ) as $key ) {
+ if ( !isset( $arr[$key] ) )
+ unset( $isec[$key] );
+ }
+ }
+
+ return $isec;
+ } else {
+ $res = array();
+ foreach ( array_keys( $isec ) as $key ) {
+ if ( isset( $keys[$key] ) )
+ $res[$key] = $isec[$key];
+ }
+
+ return $res;
+ }
+ }
+}
+
// Support for Wietse Venema's taint feature
if ( !function_exists( 'istainted' ) ) {
function istainted( $var ) {
@@ -130,15 +262,6 @@ function wfArrayDiff2_cmp( $a, $b ) {
}
/**
- * Wrapper for clone(), for compatibility with PHP4-friendly extensions.
- * PHP 5 won't let you declare a 'clone' function, even conditionally,
- * so it has to be a wrapper with a different name.
- */
-function wfClone( $object ) {
- return clone( $object );
-}
-
-/**
* Seed Mersenne Twister
* No-op for compatibility; only necessary in PHP < 4.2.0
*/
@@ -207,17 +330,18 @@ function wfUrlencode( $s ) {
*/
function wfDebug( $text, $logonly = false ) {
global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly, $wgDebugRawPage;
- global $wgDebugLogPrefix;
+ global $wgDebugLogPrefix, $wgShowDebug;
static $recursion = 0;
static $cache = array(); // Cache of unoutputted messages
+ $text = wfDebugTimer() . $text;
# 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 ) {
+ if ( ( $wgDebugComments || $wgShowDebug ) && !$logonly ) {
$cache[] = $text;
if ( !isset( $wgOut ) ) {
@@ -236,7 +360,7 @@ function wfDebug( $text, $logonly = false ) {
array_map( array( $wgOut, 'debug' ), $cache );
$cache = array();
}
- if ( '' != $wgDebugLogFile && !$wgProfileOnly ) {
+ if ( $wgDebugLogFile != '' && !$wgProfileOnly ) {
# Strip unprintables; they can switch terminal modes when binary data
# gets dumped, which is pretty annoying.
$text = preg_replace( '![\x00-\x08\x0b\x0c\x0e-\x1f]!', ' ', $text );
@@ -245,9 +369,24 @@ function wfDebug( $text, $logonly = false ) {
}
}
+function wfDebugTimer() {
+ global $wgDebugTimestamps;
+ if ( !$wgDebugTimestamps ) return '';
+ static $start = null;
+
+ if ( $start === null ) {
+ $start = microtime( true );
+ $prefix = "\n$start";
+ } else {
+ $prefix = sprintf( "%6.4f", microtime( true ) - $start );
+ }
+
+ return $prefix . ' ';
+}
+
/**
* Send a line giving PHP memory usage.
- * @param $exact Bool : print exact values instead of kilobytes (default: false)
+ * @param $exact Bool: print exact values instead of kilobytes (default: false)
*/
function wfDebugMem( $exact = false ) {
$mem = memory_get_usage();
@@ -310,13 +449,18 @@ function wfErrorLog( $text, $file ) {
// IPv6 bracketed host
$protocol = $m[1];
$host = $m[2];
- $port = $m[3];
+ $port = intval( $m[3] );
$prefix = isset( $m[4] ) ? $m[4] : false;
+ $domain = AF_INET6;
} elseif ( preg_match( '!^(tcp|udp):(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $file, $m ) ) {
$protocol = $m[1];
$host = $m[2];
- $port = $m[3];
+ if ( !IP::isIPv4( $host ) ) {
+ $host = gethostbyname( $host );
+ }
+ $port = intval( $m[3] );
$prefix = isset( $m[4] ) ? $m[4] : false;
+ $domain = AF_INET;
} else {
throw new MWException( __METHOD__.": Invalid UDP specification" );
}
@@ -328,12 +472,12 @@ function wfErrorLog( $text, $file ) {
}
}
- $sock = fsockopen( "$protocol://$host", $port );
+ $sock = socket_create( $domain, SOCK_DGRAM, SOL_UDP );
if ( !$sock ) {
return;
}
- fwrite( $sock, $text );
- fclose( $sock );
+ socket_sendto( $sock, $text, strlen( $text ), 0, $host, $port );
+ socket_close( $sock );
} else {
wfSuppressWarnings();
$exists = file_exists( $file );
@@ -374,7 +518,7 @@ function wfLogProfilingData() {
$log = sprintf( "%s\t%04.3f\t%s\n",
gmdate( 'YmdHis' ), $elapsed,
urldecode( $wgRequest->getRequestURL() . $forward ) );
- if ( '' != $wgDebugLogFile && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) {
+ if ( $wgDebugLogFile != '' && ( $wgRequest->getVal('action') != 'raw' || $wgDebugRawPage ) ) {
wfErrorLog( $log . $prof, $wgDebugLogFile );
}
}
@@ -391,7 +535,7 @@ function wfReadOnly() {
if ( !is_null( $wgReadOnly ) ) {
return (bool)$wgReadOnly;
}
- if ( '' == $wgReadOnlyFile ) {
+ if ( $wgReadOnlyFile == '' ) {
return false;
}
// Set $wgReadOnly for faster access next time
@@ -555,10 +699,10 @@ function wfMsgNoDBForContent( $key ) {
* @param $args
* @param $useDB Boolean
* @param $transform Boolean: Whether or not to transform the message.
- * @param $forContent Boolean
+ * @param $forContent Mixed: Language code, or false for user lang, true for content lang.
* @return String: the requested message.
*/
-function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform = true ) {
+function wfMsgReal( $key, $args, $useDB = true, $forContent = false, $transform = true ) {
wfProfileIn( __METHOD__ );
$message = wfMsgGetKey( $key, $useDB, $forContent, $transform );
$message = wfMsgReplaceArgs( $message, $args );
@@ -570,7 +714,7 @@ function wfMsgReal( $key, $args, $useDB = true, $forContent=false, $transform =
* This function provides the message source for messages to be edited which are *not* stored in the database.
* @param $key String:
*/
-function wfMsgWeirdKey ( $key ) {
+function wfMsgWeirdKey( $key ) {
$source = wfMsgGetKey( $key, false, true, false );
if ( wfEmptyMsg( $key, $source ) )
return "";
@@ -580,11 +724,11 @@ function wfMsgWeirdKey ( $key ) {
/**
* Fetch a message string value, but don't replace any keys yet.
- * @param string $key
- * @param bool $useDB
- * @param string $langcode Code of the language to get the message for, or
- * behaves as a content language switch if it is a
- * boolean.
+ * @param $key String
+ * @param $useDB Bool
+ * @param $langCode String: Code of the language to get the message for, or
+ * behaves as a content language switch if it is a boolean.
+ * @param $transform Boolean: whether to parse magic words, etc.
* @return string
* @private
*/
@@ -619,8 +763,8 @@ function wfMsgGetKey( $key, $useDB, $langCode = false, $transform = true ) {
/**
* Replace message parameter keys on the given formatted output.
*
- * @param string $message
- * @param array $args
+ * @param $message String
+ * @param $args Array
* @return string
* @private
*/
@@ -651,7 +795,7 @@ function wfMsgReplaceArgs( $message, $args ) {
* to pre-escape them if you really do want plaintext, or just wrap
* the whole thing in htmlspecialchars().
*
- * @param string $key
+ * @param $key String
* @param string ... parameters
* @return string
*/
@@ -668,7 +812,7 @@ function wfMsgHtml( $key ) {
* to pre-escape them if you really do want plaintext, or just wrap
* the whole thing in htmlspecialchars().
*
- * @param string $key
+ * @param $key String
* @param string ... parameters
* @return string
*/
@@ -681,8 +825,8 @@ function wfMsgWikiHtml( $key ) {
/**
* Returns message in the requested format
- * @param string $key Key of the message
- * @param array $options Processing rules. Can take the following options:
+ * @param $key String: key of the message
+ * @param $options Array: processing rules. Can take the following options:
* <i>parse</i>: parses wikitext to html
* <i>parseinline</i>: parses wikitext to html and removes the surrounding
* p's added by parser or tidy
@@ -708,12 +852,12 @@ function wfMsgExt( $key, $options ) {
foreach( $options as $arrayKey => $option ) {
if( !preg_match( '/^[0-9]+|language$/', $arrayKey ) ) {
# An unknown index, neither numeric nor "language"
- trigger_error( "wfMsgExt called with incorrect parameter key $arrayKey", E_USER_WARNING );
+ wfWarn( "wfMsgExt called with incorrect parameter key $arrayKey", 1, E_USER_WARNING );
} elseif( preg_match( '/^[0-9]+$/', $arrayKey ) && !in_array( $option,
array( 'parse', 'parseinline', 'escape', 'escapenoentities',
'replaceafter', 'parsemag', 'content' ) ) ) {
# A numeric index with unknown value
- trigger_error( "wfMsgExt called with incorrect parameter $option", E_USER_WARNING );
+ wfWarn( "wfMsgExt called with incorrect parameter $option", 1, E_USER_WARNING );
}
}
@@ -807,7 +951,7 @@ function wfErrorExit() {
/**
* Print a simple message and die, returning nonzero to the shell if any.
* Plain die() fails to return nonzero to the shell if you pass a string.
- * @param string $msg
+ * @param $msg String
*/
function wfDie( $msg='' ) {
echo $msg;
@@ -818,7 +962,7 @@ function wfDie( $msg='' ) {
* 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.
+ * @param $msg String: message shown when dieing.
*/
function wfDebugDieBacktrace( $msg = '' ) {
throw new MWException( $msg );
@@ -852,21 +996,21 @@ function wfHostname() {
return $host;
}
- /**
- * Returns a HTML comment with the elapsed time since request.
- * This method has no side effects.
- * @return string
- */
- function wfReportTime() {
- global $wgRequestTime, $wgShowHostnames;
+/**
+ * Returns a HTML comment with the elapsed time since request.
+ * This method has no side effects.
+ * @return string
+ */
+function wfReportTime() {
+ global $wgRequestTime, $wgShowHostnames;
- $now = wfTime();
- $elapsed = $now - $wgRequestTime;
+ $now = wfTime();
+ $elapsed = $now - $wgRequestTime;
- return $wgShowHostnames
- ? sprintf( "<!-- Served by %s in %01.3f secs. -->", wfHostname(), $elapsed )
- : sprintf( "<!-- Served in %01.3f secs. -->", $elapsed );
- }
+ return $wgShowHostnames
+ ? sprintf( "<!-- Served by %s in %01.3f secs. -->", wfHostname(), $elapsed )
+ : sprintf( "<!-- Served in %01.3f secs. -->", $elapsed );
+}
/**
* Safety wrapper for debug_backtrace().
@@ -974,18 +1118,19 @@ function wfShowingResultsNum( $offset, $limit, $num ) {
/**
* Generate (prev x| next x) (20|50|100...) type links for paging
- * @param $offset string
- * @param $limit int
- * @param $link string
- * @param $query string, optional URL query parameter string
- * @param $atend bool, optional param for specified if this is the last page
+ * @param $offset String
+ * @param $limit Integer
+ * @param $link String
+ * @param $query String: optional URL query parameter string
+ * @param $atend Bool: optional param for specified if this is the last page
*/
function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
global $wgLang;
$fmtLimit = $wgLang->formatNum( $limit );
+ // FIXME: Why on earth this needs one message for the text and another one for tooltip??
# Get prev/next link display text
- $prev = wfMsgHtml( 'prevn', $fmtLimit );
- $next = wfMsgHtml( 'nextn', $fmtLimit );
+ $prev = wfMsgExt( 'prevn', array('parsemag','escape'), $fmtLimit );
+ $next = wfMsgExt( 'nextn', array('parsemag','escape'), $fmtLimit );
# Get prev/next link title text
$pTitle = wfMsgExt( 'prevn-title', array('parsemag','escape'), $fmtLimit );
$nTitle = wfMsgExt( 'nextn-title', array('parsemag','escape'), $fmtLimit );
@@ -1029,15 +1174,15 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) {
wfNumLink( $offset, 250, $title, $query ),
wfNumLink( $offset, 500, $title, $query )
) );
- return wfMsg( 'viewprevnext', $plink, $nlink, $nums );
+ return wfMsgHtml( 'viewprevnext', $plink, $nlink, $nums );
}
/**
* Generate links for (20|50|100...) items-per-page links
- * @param $offset string
- * @param $limit int
+ * @param $offset String
+ * @param $limit Integer
* @param $title Title
- * @param $query string, optional URL query parameter string
+ * @param $query String: optional URL query parameter string
*/
function wfNumLink( $offset, $limit, $title, $query = '' ) {
global $wgLang;
@@ -1060,8 +1205,7 @@ function wfNumLink( $offset, $limit, $title, $query = '' ) {
* @return bool Whereas client accept gzip compression
*/
function wfClientAcceptsGzip() {
- global $wgUseGzip;
- if( $wgUseGzip ) {
+ if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
# FIXME: we may want to blacklist some broken browsers
$m = array();
if( preg_match(
@@ -1098,7 +1242,7 @@ function wfCheckLimits( $deflimit = 50, $optionname = 'rclimit' ) {
* not filter out characters which have special meaning only at the
* start of a line, such as "*".
*
- * @param string $text Text to be escaped
+ * @param $text String: text to be escaped
*/
function wfEscapeWikiText( $text ) {
$text = str_replace(
@@ -1170,7 +1314,7 @@ function wfSetBit( &$dest, $bit, $state = true ) {
* "days=7&limit=100". Options in the first array override options in the second.
* Options set to "" will not be output.
*/
-function wfArrayToCGI( $array1, $array2 = NULL )
+function wfArrayToCGI( $array1, $array2 = null )
{
if ( !is_null( $array2 ) ) {
$array1 = $array1 + $array2;
@@ -1178,24 +1322,25 @@ function wfArrayToCGI( $array1, $array2 = NULL )
$cgi = '';
foreach ( $array1 as $key => $value ) {
- if ( '' !== $value ) {
- if ( '' != $cgi ) {
+ if ( $value !== '' ) {
+ if ( $cgi != '' ) {
$cgi .= '&';
}
- if(is_array($value))
- {
+ if ( is_array( $value ) ) {
$firstTime = true;
- foreach($value as $v)
- {
- $cgi .= ($firstTime ? '' : '&') .
+ foreach ( $value as $v ) {
+ $cgi .= ( $firstTime ? '' : '&') .
urlencode( $key . '[]' ) . '=' .
urlencode( $v );
$firstTime = false;
}
- }
- else
+ } else {
+ if ( is_object( $value ) ) {
+ $value = $value->__toString();
+ }
$cgi .= urlencode( $key ) . '=' .
urlencode( $value );
+ }
}
}
return $cgi;
@@ -1208,7 +1353,7 @@ function wfArrayToCGI( $array1, $array2 = NULL )
* arrays. Of course, keys and values are urldecode()d. Don't try passing in-
* valid query strings, or it will explode.
*
- * @param $query string Query string
+ * @param $query String: query string
* @return array Array version of input
*/
function wfCgiToArray( $query ) {
@@ -1233,11 +1378,14 @@ function wfCgiToArray( $query ) {
* Append a query string to an existing URL, which may or may not already
* have query string parameters already. If so, they will be combined.
*
- * @param string $url
- * @param string $query
+ * @param $url String
+ * @param $query Mixed: string or associative array
* @return string
*/
function wfAppendQuery( $url, $query ) {
+ if ( is_array( $query ) ) {
+ $query = wfArrayToCGI( $query );
+ }
if( $query != '' ) {
if( false === strpos( $url, '?' ) ) {
$url .= '?';
@@ -1250,9 +1398,13 @@ function wfAppendQuery( $url, $query ) {
}
/**
- * Expand a potentially local URL to a fully-qualified URL.
- * Assumes $wgServer is correct. :)
- * @param string $url, either fully-qualified or a local path + query
+ * Expand a potentially local URL to a fully-qualified URL. Assumes $wgServer
+ * is correct. Also doesn't handle any type of relative URL except one
+ * starting with a single "/": this won't work with current-path-relative URLs
+ * like "subdir/foo.html", protocol-relative URLs like
+ * "//en.wikipedia.org/wiki/", etc. TODO: improve this!
+ *
+ * @param $url String: either fully-qualified or a local path + query
* @return string Fully-qualified URL
*/
function wfExpandUrl( $url ) {
@@ -1387,12 +1539,16 @@ 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
+ * @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' ) {
+ if ($before == $after) {
+ return '';
+ }
+
global $wgDiff;
# This check may also protect against code injection in
@@ -1498,7 +1654,7 @@ function wfHttpError( $code, $label, $desc ) {
* Note that some PHP configuration options may add output buffer
* layers which cannot be removed; these are left in place.
*
- * @param bool $resetGzipEncoding
+ * @param $resetGzipEncoding Bool
*/
function wfResetOutputBuffers( $resetGzipEncoding=true ) {
if( $resetGzipEncoding ) {
@@ -1583,8 +1739,8 @@ function wfAcceptToPrefs( $accept, $def = '*/*' ) {
* Returns the matching MIME type (or wildcard) if a match, otherwise
* NULL if no match.
*
- * @param string $type
- * @param array $avail
+ * @param $type String
+ * @param $avail Array
* @return string
* @private
*/
@@ -1598,7 +1754,7 @@ function mimeTypeMatch( $type, $avail ) {
} elseif( array_key_exists( '*/*', $avail ) ) {
return '*/*';
} else {
- return NULL;
+ return null;
}
}
}
@@ -1609,8 +1765,8 @@ function mimeTypeMatch( $type, $avail ) {
* array of type to preference (preference is a float between 0.0 and 1.0).
* Wildcards in the types are acceptable.
*
- * @param array $cprefs Client's acceptable type list
- * @param array $sprefs Server's offered types
+ * @param $cprefs Array: client's acceptable type list
+ * @param $sprefs Array: server's offered types
* @return string
*
* @todo FIXME: doesn't handle params like 'text/plain; charset=UTF-8'
@@ -1640,7 +1796,7 @@ function wfNegotiateType( $cprefs, $sprefs ) {
}
$bestq = 0;
- $besttype = NULL;
+ $besttype = null;
foreach( array_keys( $combine ) as $type ) {
if( $combine[$type] > $bestq ) {
@@ -1755,12 +1911,13 @@ define('TS_POSTGRES', 7);
define('TS_DB2', 8);
/**
- * @param mixed $outputtype A timestamp in one of the supported formats, the
- * function will autodetect which format is supplied
- * and act accordingly.
- * @return string Time in the format specified in $outputtype
+ * @param $outputtype Mixed: A timestamp in one of the supported formats, the
+ * function will autodetect which format is supplied and act
+ * accordingly.
+ * @param $ts Mixed: the timestamp to convert or 0 for the current timestamp
+ * @return String: in the format specified in $outputtype
*/
-function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
+function wfTimestamp( $outputtype = TS_UNIX, $ts = 0 ) {
$uts = 0;
$da = array();
if ($ts==0) {
@@ -1774,8 +1931,8 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
} 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)) {
- # TS_ORACLE
+ } elseif (preg_match('/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts)) {
+ # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6
$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})(?:\.*\d*)?Z$/', $ts, $da)) {
@@ -1812,7 +1969,8 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
case TS_RFC2822:
return gmdate( 'D, d M Y H:i:s', $uts ) . ' GMT';
case TS_ORACLE:
- return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00';
+ return gmdate( 'd-m-Y H:i:s.000000', $uts);
+ //return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00';
case TS_POSTGRES:
return gmdate( 'Y-m-d H:i:s', $uts) . ' GMT';
case TS_DB2:
@@ -1825,9 +1983,9 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) {
/**
* Return a formatted timestamp, or null if input is null.
* For dealing with nullable timestamp columns in the database.
- * @param int $outputtype
- * @param string $ts
- * @return string
+ * @param $outputtype Integer
+ * @param $ts String
+ * @return String
*/
function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
if( is_null( $ts ) ) {
@@ -1840,7 +1998,7 @@ function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
/**
* Check if the operating system is Windows
*
- * @return bool True if it's Windows, False otherwise.
+ * @return Bool: true if it's Windows, False otherwise.
*/
function wfIsWindows() {
if (substr(php_uname(), 0, 7) == 'Windows') {
@@ -1968,16 +2126,20 @@ function &wfGetMimeMagic() {
}
/**
- * Tries to get the system directory for temporary files.
- * The TMPDIR, TMP, and TEMP environment variables are checked in sequence,
- * and if none are set /tmp is returned as the generic Unix default.
+ * Tries to get the system directory for temporary files. For PHP >= 5.2.1,
+ * we'll use sys_get_temp_dir(). The TMPDIR, TMP, and TEMP environment
+ * variables are then checked in sequence, and if none are set /tmp is
+ * returned as the generic Unix default.
*
* NOTE: When possible, use the tempfile() function to create temporary
* files to avoid race conditions on file creation, etc.
*
- * @return string
+ * @return String
*/
function wfTempDir() {
+ if( function_exists( 'sys_get_temp_dir' ) ) {
+ return sys_get_temp_dir();
+ }
foreach( array( 'TMPDIR', 'TMP', 'TEMP' ) as $var ) {
$tmp = getenv( $var );
if( $tmp && file_exists( $tmp ) && is_dir( $tmp ) && is_writable( $tmp ) ) {
@@ -1991,9 +2153,9 @@ function wfTempDir() {
/**
* Make directory, and make all parent directories if they don't exist
*
- * @param string $dir Full path to directory to create
- * @param int $mode Chmod value to use, default is $wgDirectoryMode
- * @param string $caller Optional caller param for debugging.
+ * @param $dir String: full path to directory to create
+ * @param $mode Integer: chmod value to use, default is $wgDirectoryMode
+ * @param $caller String: optional caller param for debugging.
* @return bool
*/
function wfMkdirParents( $dir, $mode = null, $caller = null ) {
@@ -2006,10 +2168,17 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) {
if( strval( $dir ) === '' || file_exists( $dir ) )
return true;
+ $dir = str_replace( array( '\\', '/' ), DIRECTORY_SEPARATOR, $dir );
+
if ( is_null( $mode ) )
$mode = $wgDirectoryMode;
- return mkdir( $dir, $mode, true ); // PHP5 <3
+ $ok = mkdir( $dir, $mode, true ); // PHP5 <3
+ if( !$ok ) {
+ // PHP doesn't report the path in its warning message, so add our own to aid in diagnosis.
+ trigger_error( __FUNCTION__ . ": failed to mkdir \"$dir\" mode $mode", E_USER_WARNING );
+ }
+ return $ok;
}
/**
@@ -2040,9 +2209,9 @@ function wfIncrStats( $key ) {
}
/**
- * @param mixed $nr The number to format
- * @param int $acc The number of digits after the decimal point, default 2
- * @param bool $round Whether or not to round the value, default true
+ * @param $nr Mixed: the number to format
+ * @param $acc Integer: the number of digits after the decimal point, default 2
+ * @param $round Boolean: whether or not to round the value, default true
* @return float
*/
function wfPercent( $nr, $acc = 2, $round = true ) {
@@ -2053,9 +2222,9 @@ function wfPercent( $nr, $acc = 2, $round = true ) {
/**
* Encrypt a username/password.
*
- * @param string $userid ID of the user
- * @param string $password Password of the user
- * @return string Hashed password
+ * @param $userid Integer: ID of the user
+ * @param $password String: password of the user
+ * @return String: hashed password
* @deprecated Use User::crypt() or User::oldCrypt() instead
*/
function wfEncryptPassword( $userid, $password ) {
@@ -2081,9 +2250,9 @@ function wfAppendToArrayIfNotDefault( $key, $value, $default, &$changed ) {
* looked up didn't exist but a XHTML string, this function checks for the
* nonexistance of messages by looking at wfMsg() output
*
- * @param $msg The message key looked up
- * @param $wfMsgOut The output of wfMsg*()
- * @return bool
+ * @param $msg String: the message key looked up
+ * @param $wfMsgOut String: the output of wfMsg*()
+ * @return Boolean
*/
function wfEmptyMsg( $msg, $wfMsgOut ) {
return $wfMsgOut === htmlspecialchars( "<$msg>" );
@@ -2092,9 +2261,9 @@ function wfEmptyMsg( $msg, $wfMsgOut ) {
/**
* Find out whether or not a mixed variable exists in a string
*
- * @param mixed needle
- * @param string haystack
- * @return bool
+ * @param $needle String
+ * @param $str String
+ * @return Boolean
*/
function in_string( $needle, $str ) {
return strpos( $str, $needle ) !== false;
@@ -2109,11 +2278,15 @@ function wfSpecialList( $page, $details ) {
/**
* Returns a regular expression of url protocols
*
- * @return string
+ * @return String
*/
function wfUrlProtocols() {
global $wgUrlProtocols;
+ static $retval = null;
+ if ( !is_null( $retval ) )
+ return $retval;
+
// Support old-style $wgUrlProtocols strings, for backwards compatibility
// with LocalSettings files from 1.5
if ( is_array( $wgUrlProtocols ) ) {
@@ -2121,10 +2294,11 @@ function wfUrlProtocols() {
foreach ($wgUrlProtocols as $protocol)
$protocols[] = preg_quote( $protocol, '/' );
- return implode( '|', $protocols );
+ $retval = implode( '|', $protocols );
} else {
- return $wgUrlProtocols;
+ $retval = $wgUrlProtocols;
}
+ return $retval;
}
/**
@@ -2147,8 +2321,8 @@ function wfUrlProtocols() {
*
* I frickin' hate PHP... :P
*
- * @param string $setting
- * @return bool
+ * @param $setting String
+ * @return Bool
*/
function wfIniGetBool( $setting ) {
$val = ini_get( $setting );
@@ -2203,9 +2377,12 @@ function wfShellExec( $cmd, &$retval=null ) {
$cmd = escapeshellarg( $script ) . " $time $mem $filesize " . escapeshellarg( $cmd );
}
}
- } elseif ( php_uname( 's' ) == 'Windows NT' ) {
+ } elseif ( php_uname( 's' ) == 'Windows NT' &&
+ version_compare( PHP_VERSION, '5.3.0', '<' ) )
+ {
# This is a hack to work around PHP's flawed invocation of cmd.exe
# http://news.php.net/php.internals/21796
+ # Which is fixed in 5.3.0 :)
$cmd = '"' . $cmd . '"';
}
wfDebug( "wfShellExec: $cmd\n" );
@@ -2249,8 +2426,8 @@ function wfInitShellLocale() {
*
* @see perldoc -f use
*
- * @param mixed $version The version to check, can be a string, an integer, or
- * a float
+ * @param $req_ver Mixed: the version to check, can be a string, an integer, or
+ * a float
*/
function wfUsePHP( $req_ver ) {
$php_ver = PHP_VERSION;
@@ -2269,8 +2446,8 @@ function wfUsePHP( $req_ver ) {
*
* @see perldoc -f use
*
- * @param mixed $version The version to check, can be a string, an integer, or
- * a float
+ * @param $req_ver Mixed: the version to check, can be a string, an integer, or
+ * a float
*/
function wfUseMW( $req_ver ) {
global $wgVersion;
@@ -2294,9 +2471,9 @@ 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
+ * @param $path String
+ * @param $suffix String: to remove if present
+ * @return String
*/
function wfBaseName( $path, $suffix='' ) {
$encSuffix = ($suffix == '')
@@ -2315,9 +2492,9 @@ function wfBaseName( $path, $suffix='' ) {
* May explode on non-matching case-insensitive paths,
* funky symlinks, etc.
*
- * @param string $path Absolute destination path including target filename
- * @param string $from Absolute source path, directory only
- * @return string
+ * @param $path String: absolute destination path including target filename
+ * @param $from String: Absolute source path, directory only
+ * @return String
*/
function wfRelativePath( $path, $from ) {
// Normalize mixed input on Windows...
@@ -2359,8 +2536,9 @@ function wfRelativePath( $path, $from ) {
* Backwards array plus for people who haven't bothered to read the PHP manual
* XXX: will not darn your socks for you.
*
- * @param array $array1, [$array2, [...]]
- * @return array
+ * @param $array1 Array
+ * @param [$array2, [...]] Arrays
+ * @return Array
*/
function wfArrayMerge( $array1/* ... */ ) {
$args = func_get_args();
@@ -2407,8 +2585,8 @@ function wfMergeErrorArrays(/*...*/) {
* 2) Handles protocols that don't use :// (e.g., mailto: and news:) correctly
* 3) Adds a "delimiter" element to the array, either '://' or ':' (see (2))
*
- * @param string $url A URL to parse
- * @return array Bits of the URL in an associative array, per PHP docs
+ * @param $url String: a URL to parse
+ * @return Array: bits of the URL in an associative array, per PHP docs
*/
function wfParseUrl( $url ) {
global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php
@@ -2508,12 +2686,12 @@ function wfExplodeMarkup( $separator, $text ) {
* Supports base 2 through 36; digit values 10-36 are represented
* as lowercase letters a-z. Input is case-insensitive.
*
- * @param $input string of digits
- * @param $sourceBase int 2-36
- * @param $destBase int 2-36
- * @param $pad int 1 or greater
- * @param $lowercase bool
- * @return string or false on invalid input
+ * @param $input String: of digits
+ * @param $sourceBase Integer: 2-36
+ * @param $destBase Integer: 2-36
+ * @param $pad Integer: 1 or greater
+ * @param $lowercase Boolean
+ * @return String or false on invalid input
*/
function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true ) {
$input = strval( $input );
@@ -2590,8 +2768,8 @@ function wfBaseConvert( $input, $sourceBase, $destBase, $pad=1, $lowercase=true
/**
* Create an object with a given name and an array of construct parameters
- * @param string $name
- * @param array $p parameters
+ * @param $name String
+ * @param $p Array: parameters
*/
function wfCreateObject( $name, $p ){
$p = array_values( $p );
@@ -2619,9 +2797,9 @@ function wfCreateObject( $name, $p ){
* Alias for modularized function
* @deprecated Use Http::get() instead
*/
-function wfGetHTTP( $url, $timeout = 'default' ) {
+function wfGetHTTP( $url ) {
wfDeprecated(__FUNCTION__);
- return Http::get( $url, $timeout );
+ return Http::get( $url );
}
/**
@@ -2653,13 +2831,14 @@ function wfHttpOnlySafe() {
* Initialise php session
*/
function wfSetupSession() {
- global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly;
+ global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain,
+ $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler;
if( $wgSessionsInMemcached ) {
require_once( 'MemcachedSessions.php' );
- } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
- # If it's left on 'user' or another setting from another
- # application, it will end up failing. Try to recover.
- ini_set ( 'session.save_handler', 'files' );
+ } elseif( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) {
+ # Only set this if $wgSessionHandler isn't null and session.save_handler
+ # hasn't already been set to the desired value (that causes errors)
+ ini_set ( 'session.save_handler', $wgSessionHandler );
}
$httpOnlySafe = wfHttpOnlySafe();
wfDebugLog( 'cookie',
@@ -2685,7 +2864,7 @@ function wfSetupSession() {
/**
* Get an object from the precompiled serialized directory
*
- * @return mixed The variable on success, false on failure
+ * @return Mixed: the variable on success, false on failure
*/
function wfGetPrecompiledData( $name ) {
global $IP;
@@ -2710,12 +2889,17 @@ function wfGetCaller( $level = 2 ) {
return $caller;
}
-/** Return a string consisting all callers in stack, somewhat useful sometimes for profiling specific points */
+/**
+ * Return a string consisting all callers in stack, somewhat useful sometimes
+ * for profiling specific points
+ */
function wfGetAllCallers() {
return implode('/', array_map('wfFormatStackFrame',array_reverse(wfDebugBacktrace())));
}
-/** Return a string representation of frame */
+/**
+ * Return a string representation of frame
+ */
function wfFormatStackFrame($frame) {
return isset( $frame["class"] )?
$frame["class"]."::".$frame["function"]:
@@ -2728,6 +2912,7 @@ function wfFormatStackFrame($frame) {
function wfMemcKey( /*... */ ) {
$args = func_get_args();
$key = wfWikiID() . ':' . implode( ':', $args );
+ $key = str_replace( ' ', '_', $key );
return $key;
}
@@ -2748,16 +2933,12 @@ function wfForeignMemcKey( $db, $prefix /*, ... */ ) {
* Get an ASCII string identifying this wiki
* This is used as a prefix in memcached keys
*/
-function wfWikiID( $db = null ) {
- if( $db instanceof Database ) {
- return $db->getWikiID();
- } else {
+function wfWikiID() {
global $wgDBprefix, $wgDBname;
- if ( $wgDBprefix ) {
- return "$wgDBname-$wgDBprefix";
- } else {
- return $wgDBname;
- }
+ if ( $wgDBprefix ) {
+ return "$wgDBname-$wgDBprefix";
+ } else {
+ return $wgDBname;
}
}
@@ -2774,15 +2955,15 @@ function wfSplitWikiID( $wiki ) {
/*
* 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 $db Integer: 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
- * in one group.
+ * @param $groups Mixed: 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
+ * @param $wiki String: 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
@@ -2795,8 +2976,7 @@ function &wfGetDB( $db, $groups = array(), $wiki = false ) {
/**
* Get a load balancer object.
*
- * @param array $groups List of query groups
- * @param string $wiki Wiki ID, or false for the current wiki
+ * @param $wiki String: wiki ID, or false for the current wiki
* @return LoadBalancer
*/
function wfGetLB( $wiki = false ) {
@@ -2813,25 +2993,31 @@ function &wfGetLBFactory() {
/**
* 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
- * was created at the specified time.
- * @param mixed $flags FileRepo::FIND_ flags
- * @param boolean $bypass Bypass the file cache even if it could be used
+ * @param $title Either a string or Title object
+ * @param $options Associative array of options:
+ * 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.
+ *
+ * ignoreRedirect: If true, do not follow file redirects
+ *
+ * private: If true, return restricted (deleted) files if the current
+ * user is allowed to view them. Otherwise, such files will not
+ * be found.
+ *
+ * bypassCache: If true, do not use the process-local cache of File objects
+ *
* @return File, or false if the file does not exist
*/
-function wfFindFile( $title, $time = false, $flags = 0, $bypass = false ) {
- if( !$time && !$flags && !$bypass ) {
- return FileCache::singleton()->findFile( $title );
- } else {
- return RepoGroup::singleton()->findFile( $title, $time, $flags );
- }
+function wfFindFile( $title, $options = array() ) {
+ return RepoGroup::singleton()->findFile( $title, $options );
}
/**
* Get an object referring to a locally registered file.
* Returns a valid placeholder object if the file does not exist.
+ * @param $title Either a string or Title object
+ * @return File, or null if passed an invalid Title
*/
function wfLocalFile( $title ) {
return RepoGroup::singleton()->getLocalRepo()->newFile( $title );
@@ -2840,7 +3026,7 @@ function wfLocalFile( $title ) {
/**
* Should low-performance queries be disabled?
*
- * @return bool
+ * @return Boolean
*/
function wfQueriesMustScale() {
global $wgMiserMode;
@@ -2854,20 +3040,42 @@ function wfQueriesMustScale() {
* Get the path to a specified script file, respecting file
* extensions; this is a wrapper around $wgScriptExtension etc.
*
- * @param string $script Script filename, sans extension
- * @return string
+ * @param $script String: script filename, sans extension
+ * @return String
*/
function wfScript( $script = 'index' ) {
global $wgScriptPath, $wgScriptExtension;
return "{$wgScriptPath}/{$script}{$wgScriptExtension}";
}
+/**
+ * Get the script url.
+ *
+ * @return script url
+ */
+function wfGetScriptUrl(){
+ if( isset( $_SERVER['SCRIPT_NAME'] ) ) {
+ #
+ # as it was called, minus the query string.
+ #
+ # Some sites use Apache rewrite rules to handle subdomains,
+ # and have PHP set up in a weird way that causes PHP_SELF
+ # to contain the rewritten URL instead of the one that the
+ # outside world sees.
+ #
+ # If in this mode, use SCRIPT_URL instead, which mod_rewrite
+ # provides containing the "before" URL.
+ return $_SERVER['SCRIPT_NAME'];
+ } else {
+ return $_SERVER['URL'];
+ }
+}
/**
* Convenience function converts boolean values into "true"
* or "false" (string) values
*
- * @param bool $value
- * @return string
+ * @param $value Boolean
+ * @return String
*/
function wfBoolToStr( $value ) {
return $value ? 'true' : 'false';
@@ -2875,42 +3083,9 @@ 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).
- * @since r24808 (v1.11) Using this method of loading extension messages will not work
- * on MediaWiki prior to that
+ * @deprecated
*/
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;
- }
}
/**
@@ -2928,9 +3103,9 @@ function wfGetNull() {
/**
* Displays a maxlag error
*
- * @param string $host Server that lags the most
- * @param int $lag Maxlag (actual)
- * @param int $maxLag Maxlag (requested)
+ * @param $host String: server that lags the most
+ * @param $lag Integer: maxlag (actual)
+ * @param $maxLag Integer: maxlag (requested)
*/
function wfMaxlagError( $host, $lag, $maxLag ) {
global $wgShowHostnames;
@@ -2946,19 +3121,33 @@ function wfMaxlagError( $host, $lag, $maxLag ) {
}
/**
- * Throws an E_USER_NOTICE saying that $function is deprecated
- * @param string $function
+ * Throws a warning that $function is deprecated
+ * @param $function String
* @return null
*/
function wfDeprecated( $function ) {
- global $wgDebugLogFile;
- if ( !$wgDebugLogFile ) {
- return;
+ static $functionsWarned = array();
+ if ( !isset( $functionsWarned[$function] ) ) {
+ $functionsWarned[$function] = true;
+ wfWarn( "Use of $function is deprecated.", 2 );
}
+}
+
+/**
+ * Send a warning either to the debug log or in a PHP error depending on
+ * $wgDevelopmentWarnings
+ *
+ * @param $msg String: message to send
+ * @param $callerOffset Integer: number of itmes to go back in the backtrace to
+ * find the correct caller (1 = function calling wfWarn, ...)
+ * @param $level Integer: PHP error level; only used when $wgDevelopmentWarnings
+ * is true
+ */
+function wfWarn( $msg, $callerOffset = 1, $level = E_USER_NOTICE ) {
$callers = wfDebugBacktrace();
- if( isset( $callers[2] ) ){
- $callerfunc = $callers[2];
- $callerfile = $callers[1];
+ if( isset( $callers[$callerOffset+1] ) ){
+ $callerfunc = $callers[$callerOffset+1];
+ $callerfile = $callers[$callerOffset];
if( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ){
$file = $callerfile['file'] . ' at line ' . $callerfile['line'];
} else {
@@ -2968,11 +3157,15 @@ function wfDeprecated( $function ) {
if( isset( $callerfunc['class'] ) )
$func .= $callerfunc['class'] . '::';
$func .= @$callerfunc['function'];
- $msg = "Use of $function is deprecated. Called from $func in $file";
+ $msg .= " [Called from $func in $file]";
+ }
+
+ global $wgDevelopmentWarnings;
+ if ( $wgDevelopmentWarnings ) {
+ trigger_error( $msg, $level );
} else {
- $msg = "Use of $function is deprecated.";
+ wfDebug( "$msg\n" );
}
- wfDebug( "$msg\n" );
}
/**
@@ -2985,13 +3178,14 @@ function wfDeprecated( $function ) {
* 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
+ * @param $maxLag Integer
+ * @param $wiki mixed Wiki identifier accepted by wfGetLB
* @return null
*/
-function wfWaitForSlaves( $maxLag ) {
+function wfWaitForSlaves( $maxLag, $wiki = false ) {
if( $maxLag ) {
- $lb = wfGetLB();
- list( $host, $lag ) = $lb->getMaxLag();
+ $lb = wfGetLB( $wiki );
+ list( $host, $lag ) = $lb->getMaxLag( $wiki );
while( $lag > $maxLag ) {
$name = @gethostbyaddr( $host );
if( $name !== false ) {
@@ -3019,8 +3213,27 @@ function wfOut( $s ) {
flush();
}
+/**
+ * Count down from $n to zero on the terminal, with a one-second pause
+ * between showing each number. For use in command-line scripts.
+ */
+function wfCountDown( $n ) {
+ for ( $i = $n; $i >= 0; $i-- ) {
+ if ( $i != $n ) {
+ echo str_repeat( "\x08", strlen( $i + 1 ) );
+ }
+ echo $i;
+ flush();
+ if ( $i ) {
+ sleep( 1 );
+ }
+ }
+ echo "\n";
+}
+
/** Generate a random 32-character hexadecimal token.
- * @param mixed $salt Some sort of salt, if necessary, to add to random characters before hashing.
+ * @param $salt Mixed: some sort of salt, if necessary, to add to random
+ * characters before hashing.
*/
function wfGenerateToken( $salt = '' ) {
$salt = serialize($salt);
@@ -3030,10 +3243,122 @@ function wfGenerateToken( $salt = '' ) {
/**
* Replace all invalid characters with -
- * @param mixed $title Filename to process
+ * @param $name Mixed: filename to process
*/
function wfStripIllegalFilenameChars( $name ) {
+ global $wgIllegalFileChars;
$name = wfBaseName( $name );
- $name = preg_replace ( "/[^".Title::legalChars()."]|:/", '-', $name );
+ $name = preg_replace("/[^".Title::legalChars()."]".($wgIllegalFileChars ? "|[".$wgIllegalFileChars."]":"")."/",'-',$name);
return $name;
}
+
+/**
+ * Insert array into another array after the specified *KEY*
+ * @param $array Array: The array.
+ * @param $insert Array: The array to insert.
+ * @param $after Mixed: The key to insert after
+ */
+function wfArrayInsertAfter( $array, $insert, $after ) {
+ // Find the offset of the element to insert after.
+ $keys = array_keys($array);
+ $offsetByKey = array_flip( $keys );
+
+ $offset = $offsetByKey[$after];
+
+ // Insert at the specified offset
+ $before = array_slice( $array, 0, $offset + 1, true );
+ $after = array_slice( $array, $offset + 1, count($array)-$offset, true );
+
+ $output = $before + $insert + $after;
+
+ return $output;
+}
+
+/* Recursively converts the parameter (an object) to an array with the same data */
+function wfObjectToArray( $object, $recursive = true ) {
+ $array = array();
+ foreach ( get_object_vars($object) as $key => $value ) {
+ if ( is_object($value) && $recursive ) {
+ $value = wfObjectToArray( $value );
+ }
+
+ $array[$key] = $value;
+ }
+
+ return $array;
+}
+
+/**
+ * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit;
+ * @return Integer value memory was set to.
+ */
+
+function wfMemoryLimit () {
+ global $wgMemoryLimit;
+ $memlimit = wfShorthandToInteger( ini_get( "memory_limit" ) );
+ $conflimit = wfShorthandToInteger( $wgMemoryLimit );
+ if( $memlimit != -1 ) {
+ if( $conflimit == -1 ) {
+ wfDebug( "Removing PHP's memory limit\n" );
+ wfSuppressWarnings();
+ ini_set( "memory_limit", $conflimit );
+ wfRestoreWarnings();
+ return $conflimit;
+ } elseif ( $conflimit > $memlimit ) {
+ wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" );
+ wfSuppressWarnings();
+ ini_set( "memory_limit", $conflimit );
+ wfRestoreWarnings();
+ return $conflimit;
+ }
+ }
+ return $memlimit;
+}
+
+/**
+ * Converts shorthand byte notation to integer form
+ * @param $string String
+ * @return Integer
+ */
+function wfShorthandToInteger ( $string = '' ) {
+ $string = trim($string);
+ if( empty($string) ) { return -1; }
+ $last = strtolower($string[strlen($string)-1]);
+ $val = intval($string);
+ switch($last) {
+ case 'g':
+ $val *= 1024;
+ case 'm':
+ $val *= 1024;
+ case 'k':
+ $val *= 1024;
+ }
+
+ return $val;
+}
+
+/* Get the normalised IETF language tag
+ * @param $code String: The language code.
+ * @return $langCode String: The language code which complying with BCP 47 standards.
+ */
+function wfBCP47( $code ) {
+ $codeSegment = explode( '-', $code );
+ foreach ( $codeSegment as $segNo => $seg ) {
+ if ( count( $codeSegment ) > 0 ) {
+ // ISO 3166 country code
+ if ( ( strlen( $seg ) == 2 ) && ( $segNo > 0 ) )
+ $codeBCP[$segNo] = strtoupper( $seg );
+ // ISO 15924 script code
+ else if ( ( strlen( $seg ) == 4 ) && ( $segNo > 0 ) )
+ $codeBCP[$segNo] = ucfirst( $seg );
+ // Use lowercase for other cases
+ else
+ $codeBCP[$segNo] = strtolower( $seg );
+ } else {
+ // Use lowercase for single segment
+ $codeBCP[$segNo] = strtolower( $seg );
+ }
+ }
+ $langCode = implode ( '-' , $codeBCP );
+ return $langCode;
+}
diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php
index bd63c072..6f90b2d9 100644
--- a/includes/HTMLCacheUpdate.php
+++ b/includes/HTMLCacheUpdate.php
@@ -25,38 +25,119 @@
*/
class HTMLCacheUpdate
{
- public $mTitle, $mTable, $mPrefix;
+ public $mTitle, $mTable, $mPrefix, $mStart, $mEnd;
public $mRowsPerJob, $mRowsPerQuery;
- function __construct( $titleTo, $table ) {
+ function __construct( $titleTo, $table, $start = false, $end = false ) {
global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery;
$this->mTitle = $titleTo;
$this->mTable = $table;
+ $this->mStart = $start;
+ $this->mEnd = $end;
$this->mRowsPerJob = $wgUpdateRowsPerJob;
$this->mRowsPerQuery = $wgUpdateRowsPerQuery;
$this->mCache = $this->mTitle->getBacklinkCache();
}
public function doUpdate() {
- # Fetch the IDs
- $numRows = $this->mCache->getNumLinks( $this->mTable );
+ if ( $this->mStart || $this->mEnd ) {
+ $this->doPartialUpdate();
+ return;
+ }
- if ( $numRows != 0 ) {
- if ( $numRows > $this->mRowsPerJob ) {
- $this->insertJobs();
+ # Get an estimate of the number of rows from the BacklinkCache
+ $numRows = $this->mCache->getNumLinks( $this->mTable );
+ if ( $numRows > $this->mRowsPerJob * 2 ) {
+ # Do fast cached partition
+ $this->insertJobs();
+ } else {
+ # Get the links from the DB
+ $titleArray = $this->mCache->getLinks( $this->mTable );
+ # Check if the row count estimate was correct
+ if ( $titleArray->count() > $this->mRowsPerJob * 2 ) {
+ # Not correct, do accurate partition
+ wfDebug( __METHOD__.": row count estimate was incorrect, repartitioning\n" );
+ $this->insertJobsFromTitles( $titleArray );
} else {
- $this->invalidate();
+ $this->invalidateTitles( $titleArray );
}
}
wfRunHooks( 'HTMLCacheUpdate::doUpdate', array($this->mTitle) );
}
+ /**
+ * Update some of the backlinks, defined by a page ID range
+ */
+ protected function doPartialUpdate() {
+ $titleArray = $this->mCache->getLinks( $this->mTable, $this->mStart, $this->mEnd );
+ if ( $titleArray->count() <= $this->mRowsPerJob * 2 ) {
+ # This partition is small enough, do the update
+ $this->invalidateTitles( $titleArray );
+ } else {
+ # Partitioning was excessively inaccurate. Divide the job further.
+ # This can occur when a large number of links are added in a short
+ # period of time, say by updating a heavily-used template.
+ $this->insertJobsFromTitles( $titleArray );
+ }
+ }
+
+ /**
+ * Partition the current range given by $this->mStart and $this->mEnd,
+ * using a pre-calculated title array which gives the links in that range.
+ * Queue the resulting jobs.
+ */
+ protected function insertJobsFromTitles( $titleArray ) {
+ # We make subpartitions in the sense that the start of the first job
+ # will be the start of the parent partition, and the end of the last
+ # job will be the end of the parent partition.
+ $jobs = array();
+ $start = $this->mStart; # start of the current job
+ $numTitles = 0;
+ foreach ( $titleArray as $title ) {
+ $id = $title->getArticleID();
+ # $numTitles is now the number of titles in the current job not
+ # including the current ID
+ if ( $numTitles >= $this->mRowsPerJob ) {
+ # Add a job up to but not including the current ID
+ $params = array(
+ 'table' => $this->mTable,
+ 'start' => $start,
+ 'end' => $id - 1
+ );
+ $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params );
+ $start = $id;
+ $numTitles = 0;
+ }
+ $numTitles++;
+ }
+ # Last job
+ $params = array(
+ 'table' => $this->mTable,
+ 'start' => $start,
+ 'end' => $this->mEnd
+ );
+ $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params );
+ wfDebug( __METHOD__.": repartitioning into " . count( $jobs ) . " jobs\n" );
+
+ if ( count( $jobs ) < 2 ) {
+ # I don't think this is possible at present, but handling this case
+ # makes the code a bit more robust against future code updates and
+ # avoids a potential infinite loop of repartitioning
+ wfDebug( __METHOD__.": repartitioning failed!\n" );
+ $this->invalidateTitles( $titleArray );
+ return;
+ }
+
+ Job::batchInsert( $jobs );
+ }
+
protected function insertJobs() {
$batches = $this->mCache->partition( $this->mTable, $this->mRowsPerJob );
if ( !$batches ) {
return;
}
+ $jobs = array();
foreach ( $batches as $batch ) {
$params = array(
'table' => $this->mTable,
@@ -68,17 +149,20 @@ class HTMLCacheUpdate
Job::batchInsert( $jobs );
}
-
/**
- * Invalidate a set of pages, right now
+ * Invalidate a range of pages, right now
+ * @deprecated
*/
public function invalidate( $startId = false, $endId = false ) {
- global $wgUseFileCache, $wgUseSquid;
-
$titleArray = $this->mCache->getLinks( $this->mTable, $startId, $endId );
- if ( $titleArray->count() == 0 ) {
- return;
- }
+ $this->invalidateTitles( $titleArray );
+ }
+
+ /**
+ * Invalidate an array (or iterator) of Title objects, right now
+ */
+ protected function invalidateTitles( $titleArray ) {
+ global $wgUseFileCache, $wgUseSquid;
$dbw = wfGetDB( DB_MASTER );
$timestamp = $dbw->timestamp();
@@ -88,12 +172,20 @@ class HTMLCacheUpdate
foreach ( $titleArray as $title ) {
$ids[] = $title->getArticleID();
}
+
+ if ( !$ids ) {
+ return;
+ }
+
# Update page_touched
- $dbw->update( 'page',
- array( 'page_touched' => $timestamp ),
- array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ),
- __METHOD__
- );
+ $batches = array_chunk( $ids, $this->mRowsPerQuery );
+ foreach ( $batches as $batch ) {
+ $dbw->update( 'page',
+ array( 'page_touched' => $timestamp ),
+ array( 'page_id IN (' . $dbw->makeList( $batch ) . ')' ),
+ __METHOD__
+ );
+ }
# Update squid
if ( $wgUseSquid ) {
@@ -108,6 +200,7 @@ class HTMLCacheUpdate
}
}
}
+
}
/**
@@ -121,9 +214,9 @@ class HTMLCacheUpdateJob extends Job {
/**
* Construct a job
- * @param Title $title The title linked to
- * @param array $params Job parameters (table, start and end page_ids)
- * @param integer $id job_id
+ * @param $title Title: the title linked to
+ * @param $params Array: job parameters (table, start and end page_ids)
+ * @param $id Integer: job id
*/
function __construct( $title, $params, $id = 0 ) {
parent::__construct( 'htmlCacheUpdate', $title, $params, $id );
@@ -133,8 +226,8 @@ class HTMLCacheUpdateJob extends Job {
}
public function run() {
- $update = new HTMLCacheUpdate( $this->title, $this->table );
- $update->invalidate( $this->start, $this->end );
+ $update = new HTMLCacheUpdate( $this->title, $this->table, $this->start, $this->end );
+ $update->doUpdate();
return true;
}
}
diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php
index 68cafa24..53af1e6e 100644
--- a/includes/HTMLFileCache.php
+++ b/includes/HTMLFileCache.php
@@ -14,6 +14,7 @@
* - $wgCachePages
* - $wgCacheEpoch
* - $wgUseFileCache
+ * - $wgCacheDirectory
* - $wgFileCacheDirectory
* - $wgUseGzip
*
@@ -30,7 +31,16 @@ class HTMLFileCache {
public function fileCacheName() {
if( !$this->mFileCache ) {
- global $wgFileCacheDirectory, $wgRequest;
+ global $wgCacheDirectory, $wgFileCacheDirectory, $wgRequest;
+
+ if ( $wgFileCacheDirectory ) {
+ $dir = $wgFileCacheDirectory;
+ } elseif ( $wgCacheDirectory ) {
+ $dir = "$wgCacheDirectory/html";
+ } else {
+ throw new MWException( 'Please set $wgCacheDirectory in LocalSettings.php if you wish to use the HTML file cache' );
+ }
+
# Store raw pages (like CSS hits) elsewhere
$subdir = ($this->mType === 'raw') ? 'raw/' : '';
$key = $this->mTitle->getPrefixedDbkey();
@@ -45,7 +55,7 @@ class HTMLFileCache {
if( $this->useGzip() )
$this->mFileCache .= '.gz';
- wfDebug( " fileCacheName() - {$this->mFileCache}\n" );
+ wfDebug( __METHOD__ . ": {$this->mFileCache}\n" );
}
return $this->mFileCache;
}
@@ -96,12 +106,11 @@ class HTMLFileCache {
global $wgCacheEpoch;
if( !$this->isFileCached() ) return false;
- if( !$timestamp ) return true; // should be invalidated on change
$cachetime = $this->fileCacheTime();
$good = $timestamp <= $cachetime && $wgCacheEpoch <= $cachetime;
- wfDebug(" isFileCacheGood() - cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n");
+ wfDebug( __METHOD__ . ": cachetime $cachetime, touched '{$timestamp}' epoch {$wgCacheEpoch}, good $good\n");
return $good;
}
@@ -127,7 +136,7 @@ class HTMLFileCache {
/* Working directory to/from output */
public function loadFromFileCache() {
global $wgOut, $wgMimeType, $wgOutputEncoding, $wgContLanguageCode;
- wfDebug(" loadFromFileCache()\n");
+ wfDebug( __METHOD__ . "()\n");
$filename = $this->fileCacheName();
// Raw pages should handle cache control on their own,
// even when using file cache. This reduces hits from clients.
@@ -166,7 +175,7 @@ class HTMLFileCache {
return $text;
}
- wfDebug(" saveToFileCache()\n", false);
+ wfDebug( __METHOD__ . "()\n", false);
$this->checkCacheDirs();
diff --git a/includes/HTMLForm.php b/includes/HTMLForm.php
new file mode 100644
index 00000000..fddc887b
--- /dev/null
+++ b/includes/HTMLForm.php
@@ -0,0 +1,1391 @@
+<?php
+
+/**
+ * Object handling generic submission, CSRF protection, layout and
+ * other logic for UI forms. in a reusable manner.
+ *
+ * In order to generate the form, the HTMLForm object takes an array
+ * structure detailing the form fields available. Each element of the
+ * array is a basic property-list, including the type of field, the
+ * label it is to be given in the form, callbacks for validation and
+ * 'filtering', and other pertinent information.
+ *
+ * Field types are implemented as subclasses of the generic HTMLFormField
+ * object, and typically implement at least getInputHTML, which generates
+ * the HTML for the input field to be placed in the table.
+ *
+ * The constructor input is an associative array of $fieldname => $info,
+ * where $info is an Associative Array with any of the following:
+ *
+ * 'class' -- the subclass of HTMLFormField that will be used
+ * to create the object. *NOT* the CSS class!
+ * 'type' -- roughly translates into the <select> type attribute.
+ * if 'class' is not specified, this is used as a map
+ * through HTMLForm::$typeMappings to get the class name.
+ * 'default' -- default value when the form is displayed
+ * 'id' -- HTML id attribute
+ * 'options' -- varies according to the specific object.
+ * 'label-message' -- message key for a message to use as the label.
+ * can be an array of msg key and then parameters to
+ * the message.
+ * 'label' -- alternatively, a raw text message. Overridden by
+ * label-message
+ * 'help-message' -- message key for a message to use as a help text.
+ * can be an array of msg key and then parameters to
+ * the message.
+ * 'required' -- passed through to the object, indicating that it
+ * is a required field.
+ * 'size' -- the length of text fields
+ * 'filter-callback -- a function name to give you the chance to
+ * massage the inputted value before it's processed.
+ * @see HTMLForm::filter()
+ * 'validation-callback' -- a function name to give you the chance
+ * to impose extra validation on the field input.
+ * @see HTMLForm::validate()
+ *
+ * TODO: Document 'section' / 'subsection' stuff
+ */
+class HTMLForm {
+ static $jsAdded = false;
+
+ # A mapping of 'type' inputs onto standard HTMLFormField subclasses
+ static $typeMappings = array(
+ 'text' => 'HTMLTextField',
+ 'textarea' => 'HTMLTextAreaField',
+ 'select' => 'HTMLSelectField',
+ 'radio' => 'HTMLRadioField',
+ 'multiselect' => 'HTMLMultiSelectField',
+ 'check' => 'HTMLCheckField',
+ 'toggle' => 'HTMLCheckField',
+ 'int' => 'HTMLIntField',
+ 'float' => 'HTMLFloatField',
+ 'info' => 'HTMLInfoField',
+ 'selectorother' => 'HTMLSelectOrOtherField',
+ 'submit' => 'HTMLSubmitField',
+ 'hidden' => 'HTMLHiddenField',
+ 'edittools' => 'HTMLEditTools',
+
+ # HTMLTextField will output the correct type="" attribute automagically.
+ # There are about four zillion other HTML5 input types, like url, but
+ # we don't use those at the moment, so no point in adding all of them.
+ 'email' => 'HTMLTextField',
+ 'password' => 'HTMLTextField',
+ );
+
+ protected $mMessagePrefix;
+ protected $mFlatFields;
+ protected $mFieldTree;
+ protected $mShowReset = false;
+ public $mFieldData;
+
+ protected $mSubmitCallback;
+ protected $mValidationErrorMessage;
+
+ protected $mPre = '';
+ protected $mHeader = '';
+ protected $mPost = '';
+ protected $mId;
+
+ protected $mSubmitID;
+ protected $mSubmitName;
+ protected $mSubmitText;
+ protected $mSubmitTooltip;
+ protected $mTitle;
+
+ protected $mUseMultipart = false;
+ protected $mHiddenFields = array();
+ protected $mButtons = array();
+
+ protected $mWrapperLegend = false;
+
+ /**
+ * Build a new HTMLForm from an array of field attributes
+ * @param $descriptor Array of Field constructs, as described above
+ * @param $messagePrefix String a prefix to go in front of default messages
+ */
+ public function __construct( $descriptor, $messagePrefix='' ) {
+ $this->mMessagePrefix = $messagePrefix;
+
+ // Expand out into a tree.
+ $loadedDescriptor = array();
+ $this->mFlatFields = array();
+
+ foreach( $descriptor as $fieldname => $info ) {
+ $section = '';
+ if ( isset( $info['section'] ) )
+ $section = $info['section'];
+
+ $info['name'] = $fieldname;
+
+ if ( isset( $info['type'] ) && $info['type'] == 'file' )
+ $this->mUseMultipart = true;
+
+ $field = self::loadInputFromParameters( $info );
+ $field->mParent = $this;
+
+ $setSection =& $loadedDescriptor;
+ if( $section ) {
+ $sectionParts = explode( '/', $section );
+
+ while( count( $sectionParts ) ) {
+ $newName = array_shift( $sectionParts );
+
+ if ( !isset( $setSection[$newName] ) ) {
+ $setSection[$newName] = array();
+ }
+
+ $setSection =& $setSection[$newName];
+ }
+ }
+
+ $setSection[$fieldname] = $field;
+ $this->mFlatFields[$fieldname] = $field;
+ }
+
+ $this->mFieldTree = $loadedDescriptor;
+ }
+
+ /**
+ * Add the HTMLForm-specific JavaScript, if it hasn't been
+ * done already.
+ */
+ static function addJS() {
+ if( self::$jsAdded ) return;
+
+ global $wgOut, $wgStylePath;
+
+ $wgOut->addScriptFile( "$wgStylePath/common/htmlform.js" );
+ }
+
+ /**
+ * Initialise a new Object for the field
+ * @param $descriptor input Descriptor, as described above
+ * @return HTMLFormField subclass
+ */
+ static function loadInputFromParameters( $descriptor ) {
+ if ( isset( $descriptor['class'] ) ) {
+ $class = $descriptor['class'];
+ } elseif ( isset( $descriptor['type'] ) ) {
+ $class = self::$typeMappings[$descriptor['type']];
+ $descriptor['class'] = $class;
+ }
+
+ if( !$class ) {
+ throw new MWException( "Descriptor with no class: " . print_r( $descriptor, true ) );
+ }
+
+ $obj = new $class( $descriptor );
+
+ return $obj;
+ }
+
+ /**
+ * The here's-one-I-made-earlier option: do the submission if
+ * posted, or display the form with or without funky valiation
+ * errors
+ * @return Bool whether submission was successful.
+ */
+ function show() {
+ $html = '';
+
+ self::addJS();
+
+ # Load data from the request.
+ $this->loadData();
+
+ # Try a submission
+ global $wgUser, $wgRequest;
+ $editToken = $wgRequest->getVal( 'wpEditToken' );
+
+ $result = false;
+ if ( $wgUser->matchEditToken( $editToken ) )
+ $result = $this->trySubmit();
+
+ if( $result === true )
+ return $result;
+
+ # Display form.
+ $this->displayForm( $result );
+ return false;
+ }
+
+ /**
+ * Validate all the fields, and call the submision callback
+ * function if everything is kosher.
+ * @return Mixed Bool true == Successful submission, Bool false
+ * == No submission attempted, anything else == Error to
+ * display.
+ */
+ function trySubmit() {
+ # Check for validation
+ foreach( $this->mFlatFields as $fieldname => $field ) {
+ if ( !empty( $field->mParams['nodata'] ) )
+ continue;
+ if ( $field->validate(
+ $this->mFieldData[$fieldname],
+ $this->mFieldData )
+ !== true )
+ {
+ return isset( $this->mValidationErrorMessage )
+ ? $this->mValidationErrorMessage
+ : array( 'htmlform-invalid-input' );
+ }
+ }
+
+ $callback = $this->mSubmitCallback;
+
+ $data = $this->filterDataForSubmit( $this->mFieldData );
+
+ $res = call_user_func( $callback, $data );
+
+ return $res;
+ }
+
+ /**
+ * Set a callback to a function to do something with the form
+ * once it's been successfully validated.
+ * @param $cb String function name. The function will be passed
+ * the output from HTMLForm::filterDataForSubmit, and must
+ * return Bool true on success, Bool false if no submission
+ * was attempted, or String HTML output to display on error.
+ */
+ function setSubmitCallback( $cb ) {
+ $this->mSubmitCallback = $cb;
+ }
+
+ /**
+ * Set a message to display on a validation error.
+ * @param $msg Mixed String or Array of valid inputs to wfMsgExt()
+ * (so each entry can be either a String or Array)
+ */
+ function setValidationErrorMessage( $msg ) {
+ $this->mValidationErrorMessage = $msg;
+ }
+
+ /**
+ * Set the introductory message, overwriting any existing message.
+ * @param $msg String complete text of message to display
+ */
+ function setIntro( $msg ) { $this->mPre = $msg; }
+
+ /**
+ * Add introductory text.
+ * @param $msg String complete text of message to display
+ */
+ function addPreText( $msg ) { $this->mPre .= $msg; }
+
+ /**
+ * Add header text, inside the form.
+ * @param $msg String complete text of message to display
+ */
+ function addHeaderText( $msg ) { $this->mHeader .= $msg; }
+
+ /**
+ * Add text to the end of the display.
+ * @param $msg String complete text of message to display
+ */
+ function addPostText( $msg ) { $this->mPost .= $msg; }
+
+ /**
+ * Add a hidden field to the output
+ * @param $name String field name
+ * @param $value String field value
+ */
+ public function addHiddenField( $name, $value ){
+ $this->mHiddenFields[ $name ] = $value;
+ }
+
+ public function addButton( $name, $value, $id=null, $attribs=null ){
+ $this->mButtons[] = compact( 'name', 'value', 'id', 'attribs' );
+ }
+
+ /**
+ * Display the form (sending to wgOut), with an appropriate error
+ * message or stack of messages, and any validation errors, etc.
+ * @param $submitResult Mixed output from HTMLForm::trySubmit()
+ */
+ function displayForm( $submitResult ) {
+ global $wgOut;
+
+ if ( $submitResult !== false ) {
+ $this->displayErrors( $submitResult );
+ }
+
+ $html = ''
+ . $this->mHeader
+ . $this->getBody()
+ . $this->getHiddenFields()
+ . $this->getButtons()
+ ;
+
+ $html = $this->wrapForm( $html );
+
+ $wgOut->addHTML( ''
+ . $this->mPre
+ . $html
+ . $this->mPost
+ );
+ }
+
+ /**
+ * Wrap the form innards in an actual <form> element
+ * @param $html String HTML contents to wrap.
+ * @return String wrapped HTML.
+ */
+ function wrapForm( $html ) {
+
+ # Include a <fieldset> wrapper for style, if requested.
+ if ( $this->mWrapperLegend !== false ){
+ $html = Xml::fieldset( $this->mWrapperLegend, $html );
+ }
+ # Use multipart/form-data
+ $encType = $this->mUseMultipart
+ ? 'multipart/form-data'
+ : 'application/x-www-form-urlencoded';
+ # Attributes
+ $attribs = array(
+ 'action' => $this->getTitle()->getFullURL(),
+ 'method' => 'post',
+ 'class' => 'visualClear',
+ 'enctype' => $encType,
+ );
+ if ( !empty( $this->mId ) )
+ $attribs['id'] = $this->mId;
+
+ return Html::rawElement( 'form', $attribs, $html );
+ }
+
+ /**
+ * Get the hidden fields that should go inside the form.
+ * @return String HTML.
+ */
+ function getHiddenFields() {
+ global $wgUser;
+ $html = '';
+
+ $html .= Html::hidden( 'wpEditToken', $wgUser->editToken(), array( 'id' => 'wpEditToken' ) ) . "\n";
+ $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
+
+ foreach( $this->mHiddenFields as $name => $value ){
+ $html .= Html::hidden( $name, $value ) . "\n";
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get the submit and (potentially) reset buttons.
+ * @return String HTML.
+ */
+ function getButtons() {
+ $html = '';
+
+ $attribs = array();
+
+ if ( isset( $this->mSubmitID ) )
+ $attribs['id'] = $this->mSubmitID;
+ if ( isset( $this->mSubmitName ) )
+ $attribs['name'] = $this->mSubmitName;
+ if ( isset( $this->mSubmitTooltip ) ) {
+ global $wgUser;
+ $attribs += $wgUser->getSkin()->tooltipAndAccessKeyAttribs( $this->mSubmitTooltip );
+ }
+
+ $attribs['class'] = 'mw-htmlform-submit';
+
+ $html .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
+
+ if( $this->mShowReset ) {
+ $html .= Html::element(
+ 'input',
+ array(
+ 'type' => 'reset',
+ 'value' => wfMsg( 'htmlform-reset' )
+ )
+ ) . "\n";
+ }
+
+ foreach( $this->mButtons as $button ){
+ $attrs = array(
+ 'type' => 'submit',
+ 'name' => $button['name'],
+ 'value' => $button['value']
+ );
+ if ( $button['attribs'] )
+ $attrs += $button['attribs'];
+ if( isset( $button['id'] ) )
+ $attrs['id'] = $button['id'];
+ $html .= Html::element( 'input', $attrs );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get the whole body of the form.
+ */
+ function getBody() {
+ return $this->displaySection( $this->mFieldTree );
+ }
+
+ /**
+ * Format and display an error message stack.
+ * @param $errors Mixed String or Array of message keys
+ */
+ function displayErrors( $errors ) {
+ if ( is_array( $errors ) ) {
+ $errorstr = $this->formatErrors( $errors );
+ } else {
+ $errorstr = $errors;
+ }
+
+ $errorstr = Html::rawElement( 'div', array( 'class' => 'error' ), $errorstr );
+
+ global $wgOut;
+ $wgOut->addHTML( $errorstr );
+ }
+
+ /**
+ * Format a stack of error messages into a single HTML string
+ * @param $errors Array of message keys/values
+ * @return String HTML, a <ul> list of errors
+ */
+ static function formatErrors( $errors ) {
+ $errorstr = '';
+ foreach ( $errors as $error ) {
+ if( is_array( $error ) ) {
+ $msg = array_shift( $error );
+ } else {
+ $msg = $error;
+ $error = array();
+ }
+ $errorstr .= Html::rawElement(
+ 'li',
+ null,
+ wfMsgExt( $msg, array( 'parseinline' ), $error )
+ );
+ }
+
+ $errorstr = Html::rawElement( 'ul', array(), $errorstr );
+
+ return $errorstr;
+ }
+
+ /**
+ * Set the text for the submit button
+ * @param $t String plaintext.
+ */
+ function setSubmitText( $t ) {
+ $this->mSubmitText = $t;
+ }
+
+ /**
+ * Get the text for the submit button, either customised or a default.
+ * @return unknown_type
+ */
+ function getSubmitText() {
+ return $this->mSubmitText
+ ? $this->mSubmitText
+ : wfMsg( 'htmlform-submit' );
+ }
+
+ public function setSubmitName( $name ) {
+ $this->mSubmitName = $name;
+ }
+
+ public function setSubmitTooltip( $name ) {
+ $this->mSubmitTooltip = $name;
+ }
+
+
+ /**
+ * Set the id for the submit button.
+ * @param $t String. FIXME: Integrity is *not* validated
+ */
+ function setSubmitID( $t ) {
+ $this->mSubmitID = $t;
+ }
+
+ public function setId( $id ) {
+ $this->mId = $id;
+ }
+ /**
+ * Prompt the whole form to be wrapped in a <fieldset>, with
+ * this text as its <legend> element.
+ * @param $legend String HTML to go inside the <legend> element.
+ * Will be escaped
+ */
+ public function setWrapperLegend( $legend ){ $this->mWrapperLegend = $legend; }
+
+ /**
+ * Set the prefix for various default messages
+ * TODO: currently only used for the <fieldset> legend on forms
+ * with multiple sections; should be used elsewhre?
+ * @param $p String
+ */
+ function setMessagePrefix( $p ) {
+ $this->mMessagePrefix = $p;
+ }
+
+ /**
+ * Set the title for form submission
+ * @param $t Title of page the form is on/should be posted to
+ */
+ function setTitle( $t ) {
+ $this->mTitle = $t;
+ }
+
+ /**
+ * Get the title
+ * @return Title
+ */
+ function getTitle() {
+ return $this->mTitle;
+ }
+
+ /**
+ * TODO: Document
+ * @param $fields
+ */
+ function displaySection( $fields, $sectionName = '' ) {
+ $tableHtml = '';
+ $subsectionHtml = '';
+ $hasLeftColumn = false;
+
+ foreach( $fields as $key => $value ) {
+ if ( is_object( $value ) ) {
+ $v = empty( $value->mParams['nodata'] )
+ ? $this->mFieldData[$key]
+ : $value->getDefault();
+ $tableHtml .= $value->getTableRow( $v );
+
+ if( $value->getLabel() != '&nbsp;' )
+ $hasLeftColumn = true;
+ } elseif ( is_array( $value ) ) {
+ $section = $this->displaySection( $value, $key );
+ $legend = wfMsg( "{$this->mMessagePrefix}-$key" );
+ $subsectionHtml .= Xml::fieldset( $legend, $section ) . "\n";
+ }
+ }
+
+ $classes = array();
+ if( !$hasLeftColumn ) // Avoid strange spacing when no labels exist
+ $classes[] = 'mw-htmlform-nolabel';
+ $attribs = array(
+ 'class' => implode( ' ', $classes ),
+ );
+ if ( $sectionName )
+ $attribs['id'] = Sanitizer::escapeId( "mw-htmlform-$sectionName" );
+
+ $tableHtml = Html::rawElement( 'table', $attribs,
+ Html::rawElement( 'tbody', array(), "\n$tableHtml\n" ) ) . "\n";
+
+ return $subsectionHtml . "\n" . $tableHtml;
+ }
+
+ /**
+ * Construct the form fields from the Descriptor array
+ */
+ function loadData() {
+ global $wgRequest;
+
+ $fieldData = array();
+
+ foreach( $this->mFlatFields as $fieldname => $field ) {
+ if ( !empty( $field->mParams['nodata'] ) ) continue;
+ if ( !empty( $field->mParams['disabled'] ) ) {
+ $fieldData[$fieldname] = $field->getDefault();
+ } else {
+ $fieldData[$fieldname] = $field->loadDataFromRequest( $wgRequest );
+ }
+ }
+
+ # Filter data.
+ foreach( $fieldData as $name => &$value ) {
+ $field = $this->mFlatFields[$name];
+ $value = $field->filter( $value, $this->mFlatFields );
+ }
+
+ $this->mFieldData = $fieldData;
+ }
+
+ /**
+ * Stop a reset button being shown for this form
+ * @param $suppressReset Bool set to false to re-enable the
+ * button again
+ */
+ function suppressReset( $suppressReset = true ) {
+ $this->mShowReset = !$suppressReset;
+ }
+
+ /**
+ * Overload this if you want to apply special filtration routines
+ * to the form as a whole, after it's submitted but before it's
+ * processed.
+ * @param $data
+ * @return unknown_type
+ */
+ function filterDataForSubmit( $data ) {
+ return $data;
+ }
+}
+
+/**
+ * The parent class to generate form fields. Any field type should
+ * be a subclass of this.
+ */
+abstract class HTMLFormField {
+
+ protected $mValidationCallback;
+ protected $mFilterCallback;
+ protected $mName;
+ public $mParams;
+ protected $mLabel; # String label. Set on construction
+ protected $mID;
+ protected $mDefault;
+ public $mParent;
+
+ /**
+ * This function must be implemented to return the HTML to generate
+ * the input object itself. It should not implement the surrounding
+ * table cells/rows, or labels/help messages.
+ * @param $value String the value to set the input to; eg a default
+ * text for a text input.
+ * @return String valid HTML.
+ */
+ abstract function getInputHTML( $value );
+
+ /**
+ * Override this function to add specific validation checks on the
+ * field input. Don't forget to call parent::validate() to ensure
+ * that the user-defined callback mValidationCallback is still run
+ * @param $value String the value the field was submitted with
+ * @param $alldata $all the data collected from the form
+ * @return Mixed Bool true on success, or String error to display.
+ */
+ function validate( $value, $alldata ) {
+ if ( isset( $this->mValidationCallback ) ) {
+ return call_user_func( $this->mValidationCallback, $value, $alldata );
+ }
+
+ return true;
+ }
+
+ function filter( $value, $alldata ) {
+ if( isset( $this->mFilterCallback ) ) {
+ $value = call_user_func( $this->mFilterCallback, $value, $alldata );
+ }
+
+ return $value;
+ }
+
+ /**
+ * Should this field have a label, or is there no input element with the
+ * appropriate id for the label to point to?
+ *
+ * @return bool True to output a label, false to suppress
+ */
+ protected function needsLabel() {
+ return true;
+ }
+
+ /**
+ * Get the value that this input has been set to from a posted form,
+ * or the input's default value if it has not been set.
+ * @param $request WebRequest
+ * @return String the value
+ */
+ function loadDataFromRequest( $request ) {
+ if( $request->getCheck( $this->mName ) ) {
+ return $request->getText( $this->mName );
+ } else {
+ return $this->getDefault();
+ }
+ }
+
+ /**
+ * Initialise the object
+ * @param $params Associative Array. See HTMLForm doc for syntax.
+ */
+ function __construct( $params ) {
+ $this->mParams = $params;
+
+ # Generate the label from a message, if possible
+ if( isset( $params['label-message'] ) ) {
+ $msgInfo = $params['label-message'];
+
+ if ( is_array( $msgInfo ) ) {
+ $msg = array_shift( $msgInfo );
+ } else {
+ $msg = $msgInfo;
+ $msgInfo = array();
+ }
+
+ $this->mLabel = wfMsgExt( $msg, 'parseinline', $msgInfo );
+ } elseif ( isset( $params['label'] ) ) {
+ $this->mLabel = $params['label'];
+ }
+
+ if ( isset( $params['name'] ) ) {
+ $name = $params['name'];
+ $validName = Sanitizer::escapeId( $name );
+ if( $name != $validName ) {
+ throw new MWException("Invalid name '$name' passed to " . __METHOD__ );
+ }
+ $this->mName = 'wp'.$name;
+ $this->mID = 'mw-input-'.$name;
+ }
+
+ if ( isset( $params['default'] ) ) {
+ $this->mDefault = $params['default'];
+ }
+
+ if ( isset( $params['id'] ) ) {
+ $id = $params['id'];
+ $validId = Sanitizer::escapeId( $id );
+ if( $id != $validId ) {
+ throw new MWException("Invalid id '$id' passed to " . __METHOD__ );
+ }
+ $this->mID = $id;
+ }
+
+ if ( isset( $params['validation-callback'] ) ) {
+ $this->mValidationCallback = $params['validation-callback'];
+ }
+
+ if ( isset( $params['filter-callback'] ) ) {
+ $this->mFilterCallback = $params['filter-callback'];
+ }
+ }
+
+ /**
+ * Get the complete table row for the input, including help text,
+ * labels, and whatever.
+ * @param $value String the value to set the input to.
+ * @return String complete HTML table row.
+ */
+ function getTableRow( $value ) {
+ # Check for invalid data.
+ global $wgRequest;
+
+ $errors = $this->validate( $value, $this->mParent->mFieldData );
+ if ( $errors === true || !$wgRequest->wasPosted() ) {
+ $errors = '';
+ } else {
+ $errors = Html::rawElement( 'span', array( 'class' => 'error' ), $errors );
+ }
+
+ $html = $this->getLabelHtml();
+ $html .= Html::rawElement( 'td', array( 'class' => 'mw-input' ),
+ $this->getInputHTML( $value ) ."\n$errors" );
+
+ $fieldType = get_class( $this );
+
+ $html = Html::rawElement( 'tr', array( 'class' => "mw-htmlform-field-$fieldType" ),
+ $html ) . "\n";
+
+ $helptext = null;
+ if ( isset( $this->mParams['help-message'] ) ) {
+ $msg = $this->mParams['help-message'];
+ $helptext = wfMsgExt( $msg, 'parseinline' );
+ if ( wfEmptyMsg( $msg, $helptext ) ) {
+ # Never mind
+ $helptext = null;
+ }
+ } elseif ( isset( $this->mParams['help'] ) ) {
+ $helptext = $this->mParams['help'];
+ }
+
+ if ( !is_null( $helptext ) ) {
+ $row = Html::rawElement( 'td', array( 'colspan' => 2, 'class' => 'htmlform-tip' ),
+ $helptext );
+ $row = Html::rawElement( 'tr', array(), $row );
+ $html .= "$row\n";
+ }
+
+ return $html;
+ }
+
+ function getLabel() {
+ return $this->mLabel;
+ }
+ function getLabelHtml() {
+ # Don't output a for= attribute for labels with no associated input.
+ # Kind of hacky here, possibly we don't want these to be <label>s at all.
+ $for = array();
+ if ( $this->needsLabel() ) {
+ $for['for'] = $this->mID;
+ }
+ return Html::rawElement( 'td', array( 'class' => 'mw-label' ),
+ Html::rawElement( 'label', $for, $this->getLabel() )
+ );
+ }
+
+ function getDefault() {
+ if ( isset( $this->mDefault ) ) {
+ return $this->mDefault;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the attributes required for the tooltip and accesskey.
+ *
+ * @return array Attributes
+ */
+ public function getTooltipAndAccessKey() {
+ if ( empty( $this->mParams['tooltip'] ) )
+ return array();
+
+ global $wgUser;
+ return $wgUser->getSkin()->tooltipAndAccessKeyAttribs();
+ }
+
+ /**
+ * flatten an array of options to a single array, for instance,
+ * a set of <options> inside <optgroups>.
+ * @param $options Associative Array with values either Strings
+ * or Arrays
+ * @return Array flattened input
+ */
+ public static function flattenOptions( $options ) {
+ $flatOpts = array();
+
+ foreach( $options as $key => $value ) {
+ if ( is_array( $value ) ) {
+ $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
+ } else {
+ $flatOpts[] = $value;
+ }
+ }
+
+ return $flatOpts;
+ }
+
+}
+
+class HTMLTextField extends HTMLFormField {
+
+ function getSize() {
+ return isset( $this->mParams['size'] )
+ ? $this->mParams['size']
+ : 45;
+ }
+
+ function getInputHTML( $value ) {
+ $attribs = array(
+ 'id' => $this->mID,
+ 'name' => $this->mName,
+ 'size' => $this->getSize(),
+ 'value' => $value,
+ ) + $this->getTooltipAndAccessKey();
+
+ if ( isset( $this->mParams['maxlength'] ) ) {
+ $attribs['maxlength'] = $this->mParams['maxlength'];
+ }
+
+ if ( !empty( $this->mParams['disabled'] ) ) {
+ $attribs['disabled'] = 'disabled';
+ }
+
+ # TODO: Enforce pattern, step, required, readonly on the server side as
+ # well
+ foreach ( array( 'min', 'max', 'pattern', 'title', 'step',
+ 'placeholder' ) as $param ) {
+ if ( isset( $this->mParams[$param] ) ) {
+ $attribs[$param] = $this->mParams[$param];
+ }
+ }
+ foreach ( array( 'required', 'autofocus', 'multiple', 'readonly' ) as
+ $param ) {
+ if ( isset( $this->mParams[$param] ) ) {
+ $attribs[$param] = '';
+ }
+ }
+
+ # Implement tiny differences between some field variants
+ # here, rather than creating a new class for each one which
+ # is essentially just a clone of this one.
+ if ( isset( $this->mParams['type'] ) ) {
+ switch ( $this->mParams['type'] ) {
+ case 'email':
+ $attribs['type'] = 'email';
+ break;
+ case 'int':
+ $attribs['type'] = 'number';
+ break;
+ case 'float':
+ $attribs['type'] = 'number';
+ $attribs['step'] = 'any';
+ break;
+ # Pass through
+ case 'password':
+ case 'file':
+ $attribs['type'] = $this->mParams['type'];
+ break;
+ }
+ }
+
+ return Html::element( 'input', $attribs );
+ }
+}
+class HTMLTextAreaField extends HTMLFormField {
+
+ function getCols() {
+ return isset( $this->mParams['cols'] )
+ ? $this->mParams['cols']
+ : 80;
+ }
+ function getRows() {
+ return isset( $this->mParams['rows'] )
+ ? $this->mParams['rows']
+ : 25;
+ }
+
+ function getInputHTML( $value ) {
+ $attribs = array(
+ 'id' => $this->mID,
+ 'name' => $this->mName,
+ 'cols' => $this->getCols(),
+ 'rows' => $this->getRows(),
+ ) + $this->getTooltipAndAccessKey();
+
+
+ if ( !empty( $this->mParams['disabled'] ) ) {
+ $attribs['disabled'] = 'disabled';
+ }
+ if ( !empty( $this->mParams['readonly'] ) ) {
+ $attribs['readonly'] = 'readonly';
+ }
+
+ foreach ( array( 'required', 'autofocus' ) as $param ) {
+ if ( isset( $this->mParams[$param] ) ) {
+ $attribs[$param] = '';
+ }
+ }
+
+ return Html::element( 'textarea', $attribs, $value );
+ }
+}
+
+/**
+ * A field that will contain a numeric value
+ */
+class HTMLFloatField extends HTMLTextField {
+
+ function getSize() {
+ return isset( $this->mParams['size'] )
+ ? $this->mParams['size']
+ : 20;
+ }
+
+ function validate( $value, $alldata ) {
+ $p = parent::validate( $value, $alldata );
+
+ if ( $p !== true ) return $p;
+
+ if ( floatval( $value ) != $value ) {
+ return wfMsgExt( 'htmlform-float-invalid', 'parse' );
+ }
+
+ $in_range = true;
+
+ # The "int" part of these message names is rather confusing.
+ # They make equal sense for all numbers.
+ if ( isset( $this->mParams['min'] ) ) {
+ $min = $this->mParams['min'];
+ if ( $min > $value )
+ return wfMsgExt( 'htmlform-int-toolow', 'parse', array( $min ) );
+ }
+
+ if ( isset( $this->mParams['max'] ) ) {
+ $max = $this->mParams['max'];
+ if( $max < $value )
+ return wfMsgExt( 'htmlform-int-toohigh', 'parse', array( $max ) );
+ }
+
+ return true;
+ }
+}
+
+/**
+ * A field that must contain a number
+ */
+class HTMLIntField extends HTMLFloatField {
+ function validate( $value, $alldata ) {
+ $p = parent::validate( $value, $alldata );
+
+ if ( $p !== true ) return $p;
+
+ if ( intval( $value ) != $value ) {
+ return wfMsgExt( 'htmlform-int-invalid', 'parse' );
+ }
+
+ return true;
+ }
+}
+
+/**
+ * A checkbox field
+ */
+class HTMLCheckField extends HTMLFormField {
+ function getInputHTML( $value ) {
+ if ( !empty( $this->mParams['invert'] ) )
+ $value = !$value;
+
+ $attr = $this->getTooltipAndAccessKey();
+ $attr['id'] = $this->mID;
+ if( !empty( $this->mParams['disabled'] ) ) {
+ $attr['disabled'] = 'disabled';
+ }
+
+ return Xml::check( $this->mName, $value, $attr ) . '&nbsp;' .
+ Html::rawElement( 'label', array( 'for' => $this->mID ), $this->mLabel );
+ }
+
+ /**
+ * For a checkbox, the label goes on the right hand side, and is
+ * added in getInputHTML(), rather than HTMLFormField::getRow()
+ */
+ function getLabel() {
+ return '&nbsp;';
+ }
+
+ function loadDataFromRequest( $request ) {
+ $invert = false;
+ if ( isset( $this->mParams['invert'] ) && $this->mParams['invert'] ) {
+ $invert = true;
+ }
+
+ // GetCheck won't work like we want for checks.
+ if( $request->getCheck( 'wpEditToken' ) ) {
+ // XOR has the following truth table, which is what we want
+ // INVERT VALUE | OUTPUT
+ // true true | false
+ // false true | true
+ // false false | false
+ // true false | true
+ return $request->getBool( $this->mName ) xor $invert;
+ } else {
+ return $this->getDefault();
+ }
+ }
+}
+
+/**
+ * A select dropdown field. Basically a wrapper for Xmlselect class
+ */
+class HTMLSelectField extends HTMLFormField {
+
+ function validate( $value, $alldata ) {
+ $p = parent::validate( $value, $alldata );
+ if( $p !== true ) return $p;
+
+ $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
+ if ( in_array( $value, $validOptions ) )
+ return true;
+ else
+ return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
+ }
+
+ function getInputHTML( $value ) {
+ $select = new XmlSelect( $this->mName, $this->mID, strval( $value ) );
+
+ # If one of the options' 'name' is int(0), it is automatically selected.
+ # because PHP sucks and things int(0) == 'some string'.
+ # Working around this by forcing all of them to strings.
+ $options = array_map( 'strval', $this->mParams['options'] );
+
+ if( !empty( $this->mParams['disabled'] ) ) {
+ $select->setAttribute( 'disabled', 'disabled' );
+ }
+
+ $select->addOptions( $options );
+
+ return $select->getHTML();
+ }
+}
+
+/**
+ * Select dropdown field, with an additional "other" textbox.
+ */
+class HTMLSelectOrOtherField extends HTMLTextField {
+ static $jsAdded = false;
+
+ function __construct( $params ) {
+ if( !in_array( 'other', $params['options'], true ) ) {
+ $params['options'][wfMsg( 'htmlform-selectorother-other' )] = 'other';
+ }
+
+ parent::__construct( $params );
+ }
+
+ static function forceToStringRecursive( $array ) {
+ if ( is_array($array) ) {
+ return array_map( array( __CLASS__, 'forceToStringRecursive' ), $array);
+ } else {
+ return strval($array);
+ }
+ }
+
+ function getInputHTML( $value ) {
+ $valInSelect = false;
+
+ if( $value !== false )
+ $valInSelect = in_array( $value,
+ HTMLFormField::flattenOptions( $this->mParams['options'] ) );
+
+ $selected = $valInSelect ? $value : 'other';
+
+ $opts = self::forceToStringRecursive( $this->mParams['options'] );
+
+ $select = new XmlSelect( $this->mName, $this->mID, $selected );
+ $select->addOptions( $opts );
+
+ $select->setAttribute( 'class', 'mw-htmlform-select-or-other' );
+
+ $tbAttribs = array( 'id' => $this->mID . '-other', 'size' => $this->getSize() );
+ if( !empty( $this->mParams['disabled'] ) ) {
+ $select->setAttribute( 'disabled', 'disabled' );
+ $tbAttribs['disabled'] = 'disabled';
+ }
+
+ $select = $select->getHTML();
+
+ if ( isset( $this->mParams['maxlength'] ) ) {
+ $tbAttribs['maxlength'] = $this->mParams['maxlength'];
+ }
+
+ $textbox = Html::input( $this->mName . '-other',
+ $valInSelect ? '' : $value,
+ 'text',
+ $tbAttribs );
+
+ return "$select<br />\n$textbox";
+ }
+
+ function loadDataFromRequest( $request ) {
+ if( $request->getCheck( $this->mName ) ) {
+ $val = $request->getText( $this->mName );
+
+ if( $val == 'other' ) {
+ $val = $request->getText( $this->mName . '-other' );
+ }
+
+ return $val;
+ } else {
+ return $this->getDefault();
+ }
+ }
+}
+
+/**
+ * Multi-select field
+ */
+class HTMLMultiSelectField extends HTMLFormField {
+
+ function validate( $value, $alldata ) {
+ $p = parent::validate( $value, $alldata );
+ if( $p !== true ) return $p;
+
+ if( !is_array( $value ) ) return false;
+
+ # If all options are valid, array_intersect of the valid options
+ # and the provided options will return the provided options.
+ $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
+
+ $validValues = array_intersect( $value, $validOptions );
+ if ( count( $validValues ) == count( $value ) )
+ return true;
+ else
+ return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
+ }
+
+ function getInputHTML( $value ) {
+ $html = $this->formatOptions( $this->mParams['options'], $value );
+
+ return $html;
+ }
+
+ function formatOptions( $options, $value ) {
+ $html = '';
+
+ $attribs = array();
+ if ( !empty( $this->mParams['disabled'] ) ) {
+ $attribs['disabled'] = 'disabled';
+ }
+
+ foreach( $options as $label => $info ) {
+ if( is_array( $info ) ) {
+ $html .= Html::rawElement( 'h1', array(), $label ) . "\n";
+ $html .= $this->formatOptions( $info, $value );
+ } else {
+ $thisAttribs = array( 'id' => $this->mID . "-$info", 'value' => $info );
+
+ $checkbox = Xml::check( $this->mName . '[]', in_array( $info, $value ),
+ $attribs + $thisAttribs );
+ $checkbox .= '&nbsp;' . Html::rawElement( 'label', array( 'for' => $this->mID . "-$info" ), $label );
+
+ $html .= $checkbox . '<br />';
+ }
+ }
+
+ return $html;
+ }
+
+ function loadDataFromRequest( $request ) {
+ # won't work with getCheck
+ if( $request->getCheck( 'wpEditToken' ) ) {
+ $arr = $request->getArray( $this->mName );
+
+ if( !$arr )
+ $arr = array();
+
+ return $arr;
+ } else {
+ return $this->getDefault();
+ }
+ }
+
+ function getDefault() {
+ if ( isset( $this->mDefault ) ) {
+ return $this->mDefault;
+ } else {
+ return array();
+ }
+ }
+
+ protected function needsLabel() {
+ return false;
+ }
+}
+
+/**
+ * Radio checkbox fields.
+ */
+class HTMLRadioField extends HTMLFormField {
+ function validate( $value, $alldata ) {
+ $p = parent::validate( $value, $alldata );
+ if( $p !== true ) return $p;
+
+ if( !is_string( $value ) && !is_int( $value ) )
+ return false;
+
+ $validOptions = HTMLFormField::flattenOptions( $this->mParams['options'] );
+
+ if ( in_array( $value, $validOptions ) )
+ return true;
+ else
+ return wfMsgExt( 'htmlform-select-badoption', 'parseinline' );
+ }
+
+ /**
+ * This returns a block of all the radio options, in one cell.
+ * @see includes/HTMLFormField#getInputHTML()
+ */
+ function getInputHTML( $value ) {
+ $html = $this->formatOptions( $this->mParams['options'], $value );
+
+ return $html;
+ }
+
+ function formatOptions( $options, $value ) {
+ $html = '';
+
+ $attribs = array();
+ if ( !empty( $this->mParams['disabled'] ) ) {
+ $attribs['disabled'] = 'disabled';
+ }
+
+ # TODO: should this produce an unordered list perhaps?
+ foreach( $options as $label => $info ) {
+ if( is_array( $info ) ) {
+ $html .= Html::rawElement( 'h1', array(), $label ) . "\n";
+ $html .= $this->formatOptions( $info, $value );
+ } else {
+ $id = Sanitizer::escapeId( $this->mID . "-$info" );
+ $html .= Xml::radio( $this->mName, $info, $info == $value,
+ $attribs + array( 'id' => $id ) );
+ $html .= '&nbsp;' .
+ Html::rawElement( 'label', array( 'for' => $id ), $label );
+
+ $html .= "<br />\n";
+ }
+ }
+
+ return $html;
+ }
+
+ protected function needsLabel() {
+ return false;
+ }
+}
+
+/**
+ * An information field (text blob), not a proper input.
+ */
+class HTMLInfoField extends HTMLFormField {
+ function __construct( $info ) {
+ $info['nodata'] = true;
+
+ parent::__construct( $info );
+ }
+
+ function getInputHTML( $value ) {
+ return !empty( $this->mParams['raw'] ) ? $value : htmlspecialchars( $value );
+ }
+
+ function getTableRow( $value ) {
+ if ( !empty( $this->mParams['rawrow'] ) ) {
+ return $value;
+ }
+
+ return parent::getTableRow( $value );
+ }
+
+ protected function needsLabel() {
+ return false;
+ }
+}
+
+class HTMLHiddenField extends HTMLFormField {
+
+ public function getTableRow( $value ){
+ $this->mParent->addHiddenField(
+ $this->mParams['name'],
+ $this->mParams['default']
+ );
+ return '';
+ }
+
+ public function getInputHTML( $value ){ return ''; }
+}
+
+class HTMLSubmitField extends HTMLFormField {
+
+ public function getTableRow( $value ){
+ $this->mParent->addButton(
+ $this->mParams['name'],
+ $this->mParams['default'],
+ isset($this->mParams['id']) ? $this->mParams['id'] : null,
+ $this->getTooltipAndAccessKey()
+ );
+ }
+
+ public function getInputHTML( $value ){ return ''; }
+}
+
+class HTMLEditTools extends HTMLFormField {
+ public function getInputHTML( $value ) {
+ return '';
+ }
+ public function getTableRow( $value ) {
+ return "<tr><td></td><td class=\"mw-input\">"
+ . '<div class="mw-editTools">'
+ . wfMsgExt( empty( $this->mParams['message'] )
+ ? 'edittools' : $this->mParams['message'],
+ array( 'parse', 'content' ) )
+ . "</div></td></tr>\n";
+ }
+}
diff --git a/includes/HistoryPage.php b/includes/HistoryPage.php
new file mode 100644
index 00000000..e515d3dd
--- /dev/null
+++ b/includes/HistoryPage.php
@@ -0,0 +1,730 @@
+<?php
+/**
+ * Page history
+ *
+ * Split off from Article.php and Skin.php, 2003-12-22
+ * @file
+ */
+
+/**
+ * This class handles printing the history page for an article. In order to
+ * be efficient, it uses timestamps rather than offsets for paging, to avoid
+ * costly LIMIT,offset queries.
+ *
+ * Construct it by passing in an Article, and call $h->history() to print the
+ * history.
+ *
+ */
+class HistoryPage {
+ const DIR_PREV = 0;
+ const DIR_NEXT = 1;
+
+ var $article, $title, $skin;
+
+ /**
+ * Construct a new HistoryPage.
+ *
+ * @param $article Article
+ */
+ function __construct( $article ) {
+ global $wgUser;
+ $this->article = $article;
+ $this->title = $article->getTitle();
+ $this->skin = $wgUser->getSkin();
+ $this->preCacheMessages();
+ }
+
+ function getArticle() {
+ return $this->article;
+ }
+
+ function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * 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 ) ) {
+ $msgs = array( 'cur', 'last', 'pipe-separator' );
+ foreach( $msgs as $msg ) {
+ $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities') );
+ }
+ }
+ }
+
+ /**
+ * Print the history page for an article.
+ * @return nothing
+ */
+ function history() {
+ global $wgOut, $wgRequest, $wgScript;
+
+ /*
+ * Allow client caching.
+ */
+ if( $wgOut->checkLastModified( $this->article->getTouched() ) )
+ return; // Client cache fresh and headers sent, nothing more to do.
+
+ wfProfileIn( __METHOD__ );
+
+ /*
+ * Setup page variables.
+ */
+ $wgOut->setPageTitle( wfMsg( 'history-title', $this->title->getPrefixedText() ) );
+ $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
+ $wgOut->setArticleFlag( false );
+ $wgOut->setArticleRelated( true );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+ $wgOut->setSyndicated( true );
+ $wgOut->setFeedAppendQuery( 'action=history' );
+ $wgOut->addScriptFile( 'history.js' );
+
+ $logPage = SpecialPage::getTitleFor( 'Log' );
+ $logLink = $this->skin->link(
+ $logPage,
+ wfMsgHtml( 'viewpagelogs' ),
+ array(),
+ array( 'page' => $this->title->getPrefixedText() ),
+ array( 'known', 'noclasses' )
+ );
+ $wgOut->setSubtitle( $logLink );
+
+ $feedType = $wgRequest->getVal( 'feed' );
+ if( $feedType ) {
+ wfProfileOut( __METHOD__ );
+ return $this->feed( $feedType );
+ }
+
+ /*
+ * Fail if article doesn't exist.
+ */
+ if( !$this->title->exists() ) {
+ $wgOut->addWikiMsg( 'nohistory' );
+ # show deletion/move log if there is an entry
+ LogEventsList::showLogExtract(
+ $wgOut,
+ array( 'delete', 'move' ),
+ $this->title->getPrefixedText(),
+ '',
+ array( 'lim' => 10,
+ 'conds' => array( "log_action != 'revision'" ),
+ 'showIfEmpty' => false,
+ 'msgKey' => array( 'moveddeleted-notice' )
+ )
+ );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ /**
+ * Add date selector to quickly get to a certain time
+ */
+ $year = $wgRequest->getInt( 'year' );
+ $month = $wgRequest->getInt( 'month' );
+ $tagFilter = $wgRequest->getVal( 'tagfilter' );
+ $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
+ /**
+ * Option to show only revisions that have been (partially) hidden via RevisionDelete
+ */
+ if ( $wgRequest->getBool( 'deleted' ) ) {
+ $conds = array("rev_deleted != '0'");
+ } else {
+ $conds = array();
+ }
+ $checkDeleted = Xml::checkLabel( wfMsg( 'history-show-deleted' ),
+ 'deleted', 'mw-show-deleted-only', $wgRequest->getBool( 'deleted' ) ) . "\n";
+
+ $action = htmlspecialchars( $wgScript );
+ $wgOut->addHTML(
+ "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" .
+ Xml::fieldset(
+ wfMsg( 'history-fieldset-title' ),
+ false,
+ array( 'id' => 'mw-history-search' )
+ ) .
+ Xml::hidden( 'title', $this->title->getPrefixedDBKey() ) . "\n" .
+ Xml::hidden( 'action', 'history' ) . "\n" .
+ Xml::dateMenu( $year, $month ) . '&nbsp;' .
+ ( $tagSelector ? ( implode( '&nbsp;', $tagSelector ) . '&nbsp;' ) : '' ) .
+ $checkDeleted .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
+ '</fieldset></form>'
+ );
+
+ wfRunHooks( 'PageHistoryBeforeList', array( &$this->article ) );
+
+ /**
+ * Do the list
+ */
+ $pager = new HistoryPager( $this, $year, $month, $tagFilter, $conds );
+ $wgOut->addHTML(
+ $pager->getNavigationBar() .
+ $pager->getBody() .
+ $pager->getNavigationBar()
+ );
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Fetch an array of revisions, specified by a given limit, offset and
+ * direction. This is now only used by the feeds. It was previously
+ * used by the main UI but that's now handled by the pager.
+ *
+ * @param $limit Integer: the limit number of revisions to get
+ * @param $offset Integer
+ * @param $direction Integer: either HistoryPage::DIR_PREV or HistoryPage::DIR_NEXT
+ * @return ResultWrapper
+ */
+ function fetchRevisions( $limit, $offset, $direction ) {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ if( $direction == HistoryPage::DIR_PREV )
+ list($dirs, $oper) = array("ASC", ">=");
+ else /* $direction == HistoryPage::DIR_NEXT */
+ list($dirs, $oper) = array("DESC", "<=");
+
+ if( $offset )
+ $offsets = array("rev_timestamp $oper '$offset'");
+ else
+ $offsets = array();
+
+ $page_id = $this->title->getArticleID();
+
+ return $dbr->select( 'revision',
+ Revision::selectFields(),
+ array_merge(array("rev_page=$page_id"), $offsets),
+ __METHOD__,
+ array( 'ORDER BY' => "rev_timestamp $dirs",
+ 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
+ );
+ }
+
+ /**
+ * Output a subscription feed listing recent edits to this page.
+ *
+ * @param $type String: feed type
+ */
+ function feed( $type ) {
+ global $wgFeedClasses, $wgRequest, $wgFeedLimit;
+ if( !FeedUtils::checkFeedOutput($type) ) {
+ return;
+ }
+
+ $feed = new $wgFeedClasses[$type](
+ $this->title->getPrefixedText() . ' - ' .
+ wfMsgForContent( 'history-feed-title' ),
+ wfMsgForContent( 'history-feed-description' ),
+ $this->title->getFullUrl( 'action=history' )
+ );
+
+ // Get a limit on number of feed entries. Provide a sane default
+ // of 10 if none is defined (but limit to $wgFeedLimit max)
+ $limit = $wgRequest->getInt( 'limit', 10 );
+ if( $limit > $wgFeedLimit || $limit < 1 ) {
+ $limit = 10;
+ }
+ $items = $this->fetchRevisions($limit, 0, HistoryPage::DIR_NEXT);
+
+ $feed->outHeader();
+ if( $items ) {
+ foreach( $items as $row ) {
+ $feed->outItem( $this->feedItem( $row ) );
+ }
+ } else {
+ $feed->outItem( $this->feedEmpty() );
+ }
+ $feed->outFooter();
+ }
+
+ function feedEmpty() {
+ global $wgOut;
+ return new FeedItem(
+ wfMsgForContent( 'nohistory' ),
+ $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ),
+ $this->title->getFullUrl(),
+ wfTimestamp( TS_MW ),
+ '',
+ $this->title->getTalkPage()->getFullUrl()
+ );
+ }
+
+ /**
+ * Generate a FeedItem object from a given revision table row
+ * Borrows Recent Changes' feed generation functions for formatting;
+ * includes a diff to the previous revision (if any).
+ *
+ * @param $row Object: database row
+ * @return FeedItem
+ */
+ function feedItem( $row ) {
+ $rev = new Revision( $row );
+ $rev->setTitle( $this->title );
+ $text = FeedUtils::formatDiffRow(
+ $this->title,
+ $this->title->getPreviousRevisionID( $rev->getId() ),
+ $rev->getId(),
+ $rev->getTimestamp(),
+ $rev->getComment()
+ );
+ if( $rev->getComment() == '' ) {
+ global $wgContLang;
+ $title = wfMsgForContent( 'history-feed-item-nocomment',
+ $rev->getUserText(),
+ $wgContLang->timeanddate( $rev->getTimestamp() ),
+ $wgContLang->date( $rev->getTimestamp() ),
+ $wgContLang->time( $rev->getTimestamp() )
+ );
+ } else {
+ $title = $rev->getUserText() .
+ wfMsgForContent( 'colon-separator' ) .
+ FeedItem::stripComment( $rev->getComment() );
+ }
+ return new FeedItem(
+ $title,
+ $text,
+ $this->title->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ),
+ $rev->getTimestamp(),
+ $rev->getUserText(),
+ $this->title->getTalkPage()->getFullUrl()
+ );
+ }
+}
+
+/**
+ * @ingroup Pager
+ */
+class HistoryPager extends ReverseChronologicalPager {
+ public $lastRow = false, $counter, $historyPage, $title, $buttons, $conds;
+ protected $oldIdChecked;
+
+ function __construct( $historyPage, $year='', $month='', $tagFilter = '', $conds = array() ) {
+ parent::__construct();
+ $this->historyPage = $historyPage;
+ $this->title = $this->historyPage->title;
+ $this->tagFilter = $tagFilter;
+ $this->getDateCond( $year, $month );
+ $this->conds = $conds;
+ }
+
+ // For hook compatibility...
+ function getArticle() {
+ return $this->historyPage->getArticle();
+ }
+
+ function getSqlComment() {
+ if ( $this->conds ) {
+ return 'history page filtered'; // potentially slow, see CR r58153
+ } else {
+ return 'history page unfiltered';
+ }
+ }
+
+ function getQueryInfo() {
+ $queryInfo = array(
+ 'tables' => array('revision'),
+ 'fields' => Revision::selectFields(),
+ 'conds' => array_merge(
+ array( 'rev_page' => $this->historyPage->title->getArticleID() ),
+ $this->conds ),
+ 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ),
+ 'join_conds' => array( 'tag_summary' => array( 'LEFT JOIN', 'ts_rev_id=rev_id' ) ),
+ );
+ ChangeTags::modifyDisplayQuery(
+ $queryInfo['tables'],
+ $queryInfo['fields'],
+ $queryInfo['conds'],
+ $queryInfo['join_conds'],
+ $queryInfo['options'],
+ $this->tagFilter
+ );
+ wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) );
+ return $queryInfo;
+ }
+
+ function getIndexField() {
+ return 'rev_timestamp';
+ }
+
+ function formatRow( $row ) {
+ if( $this->lastRow ) {
+ $latest = ($this->counter == 1 && $this->mIsFirst);
+ $firstInList = $this->counter == 1;
+ $s = $this->historyLine( $this->lastRow, $row, $this->counter++,
+ $this->title->getNotificationTimestamp(), $latest, $firstInList );
+ } else {
+ $s = '';
+ }
+ $this->lastRow = $row;
+ return $s;
+ }
+
+ /**
+ * Creates begin of history list with a submit button
+ *
+ * @return string HTML output
+ */
+ function getStartBody() {
+ global $wgScript, $wgUser, $wgOut, $wgContLang;
+ $this->lastRow = false;
+ $this->counter = 1;
+ $this->oldIdChecked = 0;
+
+ $wgOut->wrapWikiMsg( "<div class='mw-history-legend'>\n$1</div>", 'histlegend' );
+ $s = Xml::openElement( 'form', array( 'action' => $wgScript,
+ 'id' => 'mw-history-compare' ) ) . "\n";
+ $s .= Xml::hidden( 'title', $this->title->getPrefixedDbKey() ) . "\n";
+ $s .= Xml::hidden( 'action', 'historysubmit' ) . "\n";
+
+ $this->buttons = '<div>';
+ if( $wgUser->isAllowed('deleterevision') ) {
+ $float = $wgContLang->alignEnd();
+ # Note bug #20966, <button> is non-standard in IE<8
+ $this->buttons .= Xml::element( 'button',
+ array(
+ 'type' => 'submit',
+ 'name' => 'revisiondelete',
+ 'value' => '1',
+ 'style' => "float: $float;",
+ 'class' => 'mw-history-revisiondelete-button',
+ ),
+ wfMsg( 'showhideselectedversions' )
+ ) . "\n";
+ }
+ $this->buttons .= $this->submitButton( wfMsg( 'compareselectedversions'),
+ array(
+ 'class' => 'historysubmit',
+ 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
+ 'title' => wfMsg( 'tooltip-compareselectedversions' ),
+ )
+ ) . "\n";
+ $this->buttons .= '</div>';
+ $s .= $this->buttons . '<ul id="pagehistory">' . "\n";
+ return $s;
+ }
+
+ function getEndBody() {
+ if( $this->lastRow ) {
+ $latest = $this->counter == 1 && $this->mIsFirst;
+ $firstInList = $this->counter == 1;
+ if( $this->mIsBackwards ) {
+ # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
+ if( $this->mOffset == '' ) {
+ $next = null;
+ } else {
+ $next = 'unknown';
+ }
+ } else {
+ # The next row is the past-the-end row
+ $next = $this->mPastTheEndRow;
+ }
+ $s = $this->historyLine( $this->lastRow, $next, $this->counter++,
+ $this->title->getNotificationTimestamp(), $latest, $firstInList );
+ } else {
+ $s = '';
+ }
+ $s .= "</ul>\n";
+ # Add second buttons only if there is more than one rev
+ if( $this->getNumRows() > 2 ) {
+ $s .= $this->buttons;
+ }
+ $s .= '</form>';
+ return $s;
+ }
+
+ /**
+ * Creates a submit button
+ *
+ * @param $message String: text of the submit button, will be escaped
+ * @param $attributes Array: attributes
+ * @return String: HTML output for the submit button
+ */
+ function submitButton( $message, $attributes = array() ) {
+ # Disable submit button if history has 1 revision only
+ if( $this->getNumRows() > 1 ) {
+ return Xml::submitButton( $message , $attributes );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Returns a row from the history printout.
+ *
+ * @todo document some more, and maybe clean up the code (some params redundant?)
+ *
+ * @param $row Object: the database row corresponding to the previous line.
+ * @param $next Mixed: the database row corresponding to the next line.
+ * @param $counter Integer: apparently a counter of what row number we're at, counted from the top row = 1.
+ * @param $notificationtimestamp
+ * @param $latest Boolean: whether this row corresponds to the page's latest revision.
+ * @param $firstInList Boolean: whether this row corresponds to the first displayed on this history page.
+ * @return String: HTML output for the row
+ */
+ function historyLine( $row, $next, $counter = '', $notificationtimestamp = false,
+ $latest = false, $firstInList = false )
+ {
+ global $wgUser, $wgLang;
+ $rev = new Revision( $row );
+ $rev->setTitle( $this->title );
+
+ $curlink = $this->curLink( $rev, $latest );
+ $lastlink = $this->lastLink( $rev, $next, $counter );
+ $diffButtons = $this->diffButtons( $rev, $firstInList, $counter );
+ $histLinks = Html::rawElement(
+ 'span',
+ array( 'class' => 'mw-history-histlinks' ),
+ '(' . $curlink . $this->historyPage->message['pipe-separator'] . $lastlink . ') '
+ );
+ $s = $histLinks . $diffButtons;
+
+ $link = $this->revLink( $rev );
+ $classes = array();
+
+ $del = '';
+ // User can delete revisions...
+ if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ // If revision was hidden from sysops, disable the checkbox
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $del = Xml::check( 'deleterevisions', false, array( 'disabled' => 'disabled' ) );
+ // Otherwise, enable the checkbox...
+ } else {
+ $del = Xml::check( 'showhiderevisions', false,
+ array( 'name' => 'ids['.$rev->getId().']' ) );
+ }
+ // User can only view deleted revisions...
+ } else if( $rev->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) {
+ // If revision was hidden from sysops, disable the link
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $cdel = $this->getSkin()->revDeleteLinkDisabled( false );
+ // Otherwise, show the link...
+ } else {
+ $query = array( 'type' => 'revision',
+ 'target' => $this->title->getPrefixedDbkey(), 'ids' => $rev->getId() );
+ $del .= $this->getSkin()->revDeleteLink( $query,
+ $rev->isDeleted( Revision::DELETED_RESTRICTED ), false );
+ }
+ }
+ if( $del ) $s .= " $del ";
+
+ $s .= " $link";
+ $s .= " <span class='history-user'>" .
+ $this->getSkin()->revUserTools( $rev, true ) . "</span>";
+
+ if( $rev->isMinor() ) {
+ $s .= ' ' . ChangesList::flag( 'minor' );
+ }
+
+ if( !is_null( $size = $rev->getSize() ) && !$rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $s .= ' ' . $this->getSkin()->formatRevisionSize( $size );
+ }
+
+ $s .= $this->getSkin()->revComment( $rev, false, true );
+
+ if( $notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp) ) {
+ $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
+ }
+
+ $tools = array();
+
+ # Rollback and undo links
+ if( !is_null( $next ) && is_object( $next ) ) {
+ if( $latest && $this->title->userCan( 'rollback' ) && $this->title->userCan( 'edit' ) ) {
+ $tools[] = '<span class="mw-rollback-link">'.
+ $this->getSkin()->buildRollbackLink( $rev ).'</span>';
+ }
+
+ if( $this->title->quickUserCan( 'edit' )
+ && !$rev->isDeleted( Revision::DELETED_TEXT )
+ && !$next->rev_deleted & Revision::DELETED_TEXT )
+ {
+ # Create undo tooltip for the first (=latest) line only
+ $undoTooltip = $latest
+ ? array( 'title' => wfMsg( 'tooltip-undo' ) )
+ : array();
+ $undolink = $this->getSkin()->link(
+ $this->title,
+ wfMsgHtml( 'editundo' ),
+ $undoTooltip,
+ array(
+ 'action' => 'edit',
+ 'undoafter' => $next->rev_id,
+ 'undo' => $rev->getId()
+ ),
+ array( 'known', 'noclasses' )
+ );
+ $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
+ }
+ }
+
+ if( $tools ) {
+ $s .= ' (' . $wgLang->pipeList( $tools ) . ')';
+ }
+
+ # Tags
+ list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' );
+ $classes = array_merge( $classes, $newClasses );
+ $s .= " $tagSummary";
+
+ wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s, &$classes ) );
+
+ $attribs = array();
+ if ( $classes ) {
+ $attribs['class'] = implode( ' ', $classes );
+ }
+
+ return Xml::tags( 'li', $attribs, $s ) . "\n";
+ }
+
+ /**
+ * Create a link to view this revision of the page
+ *
+ * @param $rev Revision
+ * @return String
+ */
+ function revLink( $rev ) {
+ global $wgLang;
+ $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
+ $date = htmlspecialchars( $date );
+ if( !$rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $link = $this->getSkin()->link(
+ $this->title,
+ $date,
+ array(),
+ array( 'oldid' => $rev->getId() ),
+ array( 'known', 'noclasses' )
+ );
+ } else {
+ $link = "<span class=\"history-deleted\">$date</span>";
+ }
+ return $link;
+ }
+
+ /**
+ * Create a diff-to-current link for this revision for this page
+ *
+ * @param $rev Revision
+ * @param $latest Boolean: this is the latest revision of the page?
+ * @return String
+ */
+ function curLink( $rev, $latest ) {
+ $cur = $this->historyPage->message['cur'];
+ if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ return $cur;
+ } else {
+ return $this->getSkin()->link(
+ $this->title,
+ $cur,
+ array(),
+ array(
+ 'diff' => $this->title->getLatestRevID(),
+ 'oldid' => $rev->getId()
+ ),
+ array( 'known', 'noclasses' )
+ );
+ }
+ }
+
+ /**
+ * Create a diff-to-previous link for this revision for this page.
+ *
+ * @param $prevRev Revision: the previous revision
+ * @param $next Mixed: the newer revision
+ * @param $counter Integer: what row on the history list this is
+ * @return String
+ */
+ function lastLink( $prevRev, $next, $counter ) {
+ $last = $this->historyPage->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' ) {
+ # Next row probably exists but is unknown, use an oldid=prev link
+ return $this->getSkin()->link(
+ $this->title,
+ $last,
+ array(),
+ array(
+ 'diff' => $prevRev->getId(),
+ 'oldid' => 'prev'
+ ),
+ array( 'known', 'noclasses' )
+ );
+ } elseif( !$prevRev->userCan(Revision::DELETED_TEXT)
+ || !$nextRev->userCan(Revision::DELETED_TEXT) )
+ {
+ return $last;
+ } else {
+ return $this->getSkin()->link(
+ $this->title,
+ $last,
+ array(),
+ array(
+ 'diff' => $prevRev->getId(),
+ 'oldid' => $next->rev_id
+ ),
+ array( 'known', 'noclasses' )
+ );
+ }
+ }
+
+ /**
+ * Create radio buttons for page history
+ *
+ * @param $rev Revision object
+ * @param $firstInList Boolean: is this version the first one?
+ * @param $counter Integer: a counter of what row number we're at, counted from the top row = 1.
+ * @return String: HTML output for the radio buttons
+ */
+ function diffButtons( $rev, $firstInList, $counter ) {
+ if( $this->getNumRows() > 1 ) {
+ $id = $rev->getId();
+ $radio = array( 'type' => 'radio', 'value' => $id );
+ /** @todo: move title texts to javascript */
+ if( $firstInList ) {
+ $first = Xml::element( 'input',
+ array_merge( $radio, array(
+ 'style' => 'visibility:hidden',
+ 'name' => 'oldid',
+ 'id' => 'mw-oldid-null' ) )
+ );
+ $checkmark = array( 'checked' => 'checked' );
+ } else {
+ # Check visibility of old revisions
+ if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ $radio['disabled'] = 'disabled';
+ $checkmark = array(); // We will check the next possible one
+ } else if( $counter == 2 || !$this->oldIdChecked ) {
+ $checkmark = array( 'checked' => 'checked' );
+ $this->oldIdChecked = $id;
+ } else {
+ $checkmark = array();
+ }
+ $first = Xml::element( 'input',
+ array_merge( $radio, $checkmark, array(
+ 'name' => 'oldid',
+ 'id' => "mw-oldid-$id" ) ) );
+ $checkmark = array();
+ }
+ $second = Xml::element( 'input',
+ array_merge( $radio, $checkmark, array(
+ 'name' => 'diff',
+ 'id' => "mw-diff-$id" ) ) );
+ return $first . $second;
+ } else {
+ return '';
+ }
+ }
+}
+
+/**
+ * Backwards-compatibility aliases
+ */
+class PageHistory extends HistoryPage {}
+class PageHistoryPager extends HistoryPager {}
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 046a149d..dec1c442 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -32,15 +32,16 @@ function wfRunHooks($event, $args = array()) {
global $wgHooks;
+ // Return quickly in the most common case
+ if ( !isset( $wgHooks[$event] ) ) {
+ return true;
+ }
+
if (!is_array($wgHooks)) {
throw new MWException("Global hooks array is not an array!\n");
return false;
}
- if (!array_key_exists($event, $wgHooks)) {
- return true;
- }
-
if (!is_array($wgHooks[$event])) {
throw new MWException("Hooks array for event '$event' is not an array!\n");
return false;
@@ -48,59 +49,74 @@ function wfRunHooks($event, $args = array()) {
foreach ($wgHooks[$event] as $index => $hook) {
- $object = NULL;
- $method = NULL;
- $func = NULL;
- $data = NULL;
+ $object = null;
+ $method = null;
+ $func = null;
+ $data = null;
$have_data = false;
+ $closure = false;
/* $hook can be: a function, an object, an array of $function and $data,
* an array of just a function, an array of object and method, or an
* array of object, method, and data.
*/
- if (is_array($hook)) {
- if (count($hook) < 1) {
+ if ( is_array( $hook ) ) {
+ if ( count( $hook ) < 1 ) {
throw new MWException("Empty array in hooks for " . $event . "\n");
- } else if (is_object($hook[0])) {
+ } else if ( is_object( $hook[0] ) ) {
$object = $wgHooks[$event][$index][0];
- if (count($hook) < 2) {
- $method = "on" . $event;
- } else {
- $method = $hook[1];
- if (count($hook) > 2) {
- $data = $hook[2];
+ if ( $object instanceof Closure ) {
+ $closure = true;
+ if ( count( $hook ) > 1 ) {
+ $data = $hook[1];
$have_data = true;
}
+ } else {
+ if ( count( $hook ) < 2 ) {
+ $method = "on" . $event;
+ } else {
+ $method = $hook[1];
+ if ( count( $hook ) > 2 ) {
+ $data = $hook[2];
+ $have_data = true;
+ }
+ }
}
- } else if (is_string($hook[0])) {
+ } else if ( is_string( $hook[0] ) ) {
$func = $hook[0];
- if (count($hook) > 1) {
+ if ( count( $hook ) > 1) {
$data = $hook[1];
$have_data = true;
}
} else {
- var_dump( $wgHooks );
- throw new MWException("Unknown datatype in hooks for " . $event . "\n");
+ throw new MWException( "Unknown datatype in hooks for " . $event . "\n" );
}
- } else if (is_string($hook)) { # functions look like strings, too
+ } else if ( is_string( $hook ) ) { # functions look like strings, too
$func = $hook;
- } else if (is_object($hook)) {
+ } else if ( is_object( $hook ) ) {
$object = $wgHooks[$event][$index];
- $method = "on" . $event;
+ if ( $object instanceof Closure ) {
+ $closure = true;
+ } else {
+ $method = "on" . $event;
+ }
} else {
- throw new MWException("Unknown datatype in hooks for " . $event . "\n");
+ throw new MWException( "Unknown datatype in hooks for " . $event . "\n" );
}
/* We put the first data element on, if needed. */
- if ($have_data) {
+ if ( $have_data ) {
$hook_args = array_merge(array($data), $args);
} else {
$hook_args = $args;
}
- if ( isset( $object ) ) {
+ if ( $closure ) {
+ $callback = $object;
+ $func = "hook-$event-closure";
+ } elseif ( isset( $object ) ) {
$func = get_class( $object ) . '::' . $method;
$callback = array( $object, $method );
} elseif ( false !== ( $pos = strpos( $func, '::' ) ) ) {
@@ -119,12 +135,14 @@ function wfRunHooks($event, $args = array()) {
/* String return is an error; false return means stop processing. */
- if (is_string($retval)) {
+ if ( is_string( $retval ) ) {
global $wgOut;
- $wgOut->showFatalError($retval);
+ $wgOut->showFatalError( $retval );
return false;
} elseif( $retval === null ) {
- if( is_array( $callback ) ) {
+ if ( $closure ) {
+ $prettyFunc = "$event closure";
+ } elseif( is_array( $callback ) ) {
if( is_object( $callback[0] ) ) {
$prettyClass = get_class( $callback[0] );
} else {
@@ -137,7 +155,7 @@ function wfRunHooks($event, $args = array()) {
throw new MWException( "Detected bug in an extension! " .
"Hook $prettyFunc failed to return a value; " .
"should return true to continue hook processing or false to abort." );
- } else if (!$retval) {
+ } else if ( !$retval ) {
return false;
}
}
diff --git a/includes/Html.php b/includes/Html.php
new file mode 100644
index 00000000..00183b31
--- /dev/null
+++ b/includes/Html.php
@@ -0,0 +1,539 @@
+<?php
+# Copyright (C) 2009 Aryeh Gregor
+# 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 class is a collection of static functions that serve two purposes:
+ *
+ * 1) Implement any algorithms specified by HTML5, or other HTML
+ * specifications, in a convenient and self-contained way.
+ *
+ * 2) Allow HTML elements to be conveniently and safely generated, like the
+ * current Xml class but a) less confused (Xml supports HTML-specific things,
+ * but only sometimes!) and b) not necessarily confined to XML-compatible
+ * output.
+ *
+ * There are two important configuration options this class uses:
+ *
+ * $wgHtml5: If this is set to false, then all output should be valid XHTML 1.0
+ * Transitional.
+ * $wgWellFormedXml: If this is set to true, then all output should be
+ * well-formed XML (quotes on attributes, self-closing tags, etc.).
+ *
+ * This class is meant to be confined to utility functions that are called from
+ * trusted code paths. It does not do enforcement of policy like not allowing
+ * <a> elements.
+ */
+class Html {
+ # List of void elements from HTML5, section 9.1.2 as of 2009-08-10
+ private static $voidElements = array(
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'command',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'keygen',
+ 'link',
+ 'meta',
+ 'param',
+ 'source',
+ );
+
+ # Boolean attributes, which may have the value omitted entirely. Manually
+ # collected from the HTML5 spec as of 2009-08-10.
+ private static $boolAttribs = array(
+ 'async',
+ 'autobuffer',
+ 'autofocus',
+ 'autoplay',
+ 'checked',
+ 'controls',
+ 'defer',
+ 'disabled',
+ 'formnovalidate',
+ 'hidden',
+ 'ismap',
+ 'loop',
+ 'multiple',
+ 'novalidate',
+ 'open',
+ 'readonly',
+ 'required',
+ 'reversed',
+ 'scoped',
+ 'seamless',
+ );
+
+ /**
+ * Returns an HTML element in a string. The major advantage here over
+ * manually typing out the HTML is that it will escape all attribute
+ * values. If you're hardcoding all the attributes, or there are none, you
+ * should probably type out the string yourself.
+ *
+ * This is quite similar to Xml::tags(), but it implements some useful
+ * HTML-specific logic. For instance, there is no $allowShortTag
+ * parameter: the closing tag is magically omitted if $element has an empty
+ * content model. If $wgWellFormedXml is false, then a few bytes will be
+ * shaved off the HTML output as well. In the future, other HTML-specific
+ * features might be added, like allowing arrays for the values of
+ * attributes like class= and media=.
+ *
+ * @param $element string The element's name, e.g., 'a'
+ * @param $attribs array Associative array of attributes, e.g., array(
+ * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
+ * further documentation.
+ * @param $contents string The raw HTML contents of the element: *not*
+ * escaped!
+ * @return string Raw HTML
+ */
+ public static function rawElement( $element, $attribs = array(), $contents = '' ) {
+ global $wgWellFormedXml;
+ $start = self::openElement( $element, $attribs );
+ if ( in_array( $element, self::$voidElements ) ) {
+ if ( $wgWellFormedXml ) {
+ # Silly XML.
+ return substr( $start, 0, -1 ) . ' />';
+ }
+ return $start;
+ } else {
+ return "$start$contents</$element>";
+ }
+ }
+
+ /**
+ * Identical to rawElement(), but HTML-escapes $contents (like
+ * Xml::element()).
+ */
+ public static function element( $element, $attribs = array(), $contents = '' ) {
+ return self::rawElement( $element, $attribs, strtr( $contents, array(
+ # There's no point in escaping quotes, >, etc. in the contents of
+ # elements.
+ '&' => '&amp;',
+ '<' => '&lt;'
+ ) ) );
+ }
+
+ /**
+ * Identical to rawElement(), but has no third parameter and omits the end
+ * tag (and the self-closing / in XML mode for empty elements).
+ */
+ public static function openElement( $element, $attribs = array() ) {
+ global $wgHtml5;
+ $attribs = (array)$attribs;
+ # This is not required in HTML5, but let's do it anyway, for
+ # consistency and better compression.
+ $element = strtolower( $element );
+
+ # Remove HTML5-only attributes if we aren't doing HTML5, and disable
+ # form validation regardless (see bug 23769 and the more detailed
+ # comment in expandAttributes())
+ if ( $element == 'input' ) {
+ # Whitelist of types that don't cause validation. All except
+ # 'search' are valid in XHTML1.
+ $validTypes = array(
+ 'hidden',
+ 'text',
+ 'password',
+ 'checkbox',
+ 'radio',
+ 'file',
+ 'submit',
+ 'image',
+ 'reset',
+ 'button',
+ 'search',
+ );
+ if ( isset( $attribs['type'] )
+ && !in_array( $attribs['type'], $validTypes ) ) {
+ unset( $attribs['type'] );
+ }
+ if ( isset( $attribs['type'] ) && $attribs['type'] == 'search'
+ && !$wgHtml5 ) {
+ unset( $attribs['type'] );
+ }
+ # Here we're blacklisting some HTML5-only attributes...
+ $html5attribs = array(
+ 'autocomplete',
+ 'autofocus',
+ 'max',
+ 'min',
+ 'multiple',
+ 'pattern',
+ 'placeholder',
+ 'required',
+ 'step',
+ 'spellcheck',
+ );
+ foreach ( $html5attribs as $badAttr ) {
+ unset( $attribs[$badAttr] );
+ }
+ }
+ if ( !$wgHtml5 && $element == 'textarea' && isset( $attribs['maxlength'] ) ) {
+ unset( $attribs['maxlength'] );
+ }
+
+ return "<$element" . self::expandAttributes(
+ self::dropDefaults( $element, $attribs ) ) . '>';
+ }
+
+ /**
+ * Given an element name and an associative array of element attributes,
+ * return an array that is functionally identical to the input array, but
+ * possibly smaller. In particular, attributes might be stripped if they
+ * are given their default values.
+ *
+ * This method is not guaranteed to remove all redundant attributes, only
+ * some common ones and some others selected arbitrarily at random. It
+ * only guarantees that the output array should be functionally identical
+ * to the input array (currently per the HTML 5 draft as of 2009-09-06).
+ *
+ * @param $element string Name of the element, e.g., 'a'
+ * @param $attribs array Associative array of attributes, e.g., array(
+ * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for
+ * further documentation.
+ * @return array An array of attributes functionally identical to $attribs
+ */
+ private static function dropDefaults( $element, $attribs ) {
+ # Don't bother doing anything if we aren't outputting HTML5; it's too
+ # much of a pain to maintain two sets of defaults.
+ global $wgHtml5;
+ if ( !$wgHtml5 ) {
+ return $attribs;
+ }
+
+ static $attribDefaults = array(
+ 'area' => array( 'shape' => 'rect' ),
+ 'button' => array(
+ 'formaction' => 'GET',
+ 'formenctype' => 'application/x-www-form-urlencoded',
+ 'type' => 'submit',
+ ),
+ 'canvas' => array(
+ 'height' => '150',
+ 'width' => '300',
+ ),
+ 'command' => array( 'type' => 'command' ),
+ 'form' => array(
+ 'action' => 'GET',
+ 'autocomplete' => 'on',
+ 'enctype' => 'application/x-www-form-urlencoded',
+ ),
+ 'input' => array(
+ 'formaction' => 'GET',
+ 'type' => 'text',
+ 'value' => '',
+ ),
+ 'keygen' => array( 'keytype' => 'rsa' ),
+ 'link' => array( 'media' => 'all' ),
+ 'menu' => array( 'type' => 'list' ),
+ # Note: the use of text/javascript here instead of other JavaScript
+ # MIME types follows the HTML5 spec.
+ 'script' => array( 'type' => 'text/javascript' ),
+ 'style' => array(
+ 'media' => 'all',
+ 'type' => 'text/css',
+ ),
+ 'textarea' => array( 'wrap' => 'soft' ),
+ );
+
+ $element = strtolower( $element );
+
+ foreach ( $attribs as $attrib => $value ) {
+ $lcattrib = strtolower( $attrib );
+ $value = strval( $value );
+
+ # Simple checks using $attribDefaults
+ if ( isset( $attribDefaults[$element][$lcattrib] ) &&
+ $attribDefaults[$element][$lcattrib] == $value ) {
+ unset( $attribs[$attrib] );
+ }
+
+ if ( $lcattrib == 'class' && $value == '' ) {
+ unset( $attribs[$attrib] );
+ }
+ }
+
+ # More subtle checks
+ if ( $element === 'link' && isset( $attribs['type'] )
+ && strval( $attribs['type'] ) == 'text/css' ) {
+ unset( $attribs['type'] );
+ }
+ if ( $element === 'select' && isset( $attribs['size'] ) ) {
+ if ( in_array( 'multiple', $attribs )
+ || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
+ ) {
+ # A multi-select
+ if ( strval( $attribs['size'] ) == '4' ) {
+ unset( $attribs['size'] );
+ }
+ } else {
+ # Single select
+ if ( strval( $attribs['size'] ) == '1' ) {
+ unset( $attribs['size'] );
+ }
+ }
+ }
+
+ return $attribs;
+ }
+
+ /**
+ * Given an associative array of element attributes, generate a string
+ * to stick after the element name in HTML output. Like array( 'href' =>
+ * 'http://www.mediawiki.org/' ) becomes something like
+ * ' href="http://www.mediawiki.org"'. Again, this is like
+ * Xml::expandAttributes(), but it implements some HTML-specific logic.
+ * For instance, it will omit quotation marks if $wgWellFormedXml is false,
+ * and will treat boolean attributes specially.
+ *
+ * @param $attribs array Associative array of attributes, e.g., array(
+ * 'href' => 'http://www.mediawiki.org/' ). Values will be HTML-escaped.
+ * A value of false means to omit the attribute. For boolean attributes,
+ * you can omit the key, e.g., array( 'checked' ) instead of
+ * array( 'checked' => 'checked' ) or such.
+ * @return string HTML fragment that goes between element name and '>'
+ * (starting with a space if at least one attribute is output)
+ */
+ public static function expandAttributes( $attribs ) {
+ global $wgHtml5, $wgWellFormedXml;
+
+ $ret = '';
+ $attribs = (array)$attribs;
+ foreach ( $attribs as $key => $value ) {
+ if ( $value === false ) {
+ continue;
+ }
+
+ # For boolean attributes, support array( 'foo' ) instead of
+ # requiring array( 'foo' => 'meaningless' ).
+ if ( is_int( $key )
+ && in_array( strtolower( $value ), self::$boolAttribs ) ) {
+ $key = $value;
+ }
+
+ # Not technically required in HTML5, but required in XHTML 1.0,
+ # and we'd like consistency and better compression anyway.
+ $key = strtolower( $key );
+
+ # Bug 23769: Blacklist all form validation attributes for now. Current
+ # (June 2010) WebKit has no UI, so the form just refuses to submit
+ # without telling the user why, which is much worse than failing
+ # server-side validation. Opera is the only other implementation at
+ # this time, and has ugly UI, so just kill the feature entirely until
+ # we have at least one good implementation.
+ if ( in_array( $key, array( 'max', 'min', 'pattern', 'required', 'step' ) ) ) {
+ continue;
+ }
+
+ # See the "Attributes" section in the HTML syntax part of HTML5,
+ # 9.1.2.3 as of 2009-08-10. Most attributes can have quotation
+ # marks omitted, but not all. (Although a literal " is not
+ # permitted, we don't check for that, since it will be escaped
+ # anyway.)
+ #
+ # See also research done on further characters that need to be
+ # escaped: http://code.google.com/p/html5lib/issues/detail?id=93
+ $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}"
+ . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}"
+ . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}";
+ if ( $wgWellFormedXml || $value === ''
+ || preg_match( "![$badChars]!u", $value ) ) {
+ $quote = '"';
+ } else {
+ $quote = '';
+ }
+
+ if ( in_array( $key, self::$boolAttribs ) ) {
+ # In XHTML 1.0 Transitional, the value needs to be equal to the
+ # key. In HTML5, we can leave the value empty instead. If we
+ # don't need well-formed XML, we can omit the = entirely.
+ if ( !$wgWellFormedXml ) {
+ $ret .= " $key";
+ } elseif ( $wgHtml5 ) {
+ $ret .= " $key=\"\"";
+ } else {
+ $ret .= " $key=\"$key\"";
+ }
+ } else {
+ # Apparently we need to entity-encode \n, \r, \t, although the
+ # spec doesn't mention that. Since we're doing strtr() anyway,
+ # and we don't need <> escaped here, we may as well not call
+ # htmlspecialchars(). FIXME: verify that we actually need to
+ # escape \n\r\t here, and explain why, exactly.
+ #
+ # We could call Sanitizer::encodeAttribute() for this, but we
+ # don't because we're stubborn and like our marginal savings on
+ # byte size from not having to encode unnecessary quotes.
+ $map = array(
+ '&' => '&amp;',
+ '"' => '&quot;',
+ "\n" => '&#10;',
+ "\r" => '&#13;',
+ "\t" => '&#9;'
+ );
+ if ( $wgWellFormedXml ) {
+ # This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue>
+ # But reportedly it breaks some XML tools? FIXME: is this
+ # really true?
+ $map['<'] = '&lt;';
+ }
+ $ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Output a <script> tag with the given contents. TODO: do some useful
+ * escaping as well, like if $contents contains literal '</script>' or (for
+ * XML) literal "]]>".
+ *
+ * @param $contents string JavaScript
+ * @return string Raw HTML
+ */
+ public static function inlineScript( $contents ) {
+ global $wgHtml5, $wgJsMimeType, $wgWellFormedXml;
+
+ $attrs = array();
+ if ( !$wgHtml5 ) {
+ $attrs['type'] = $wgJsMimeType;
+ }
+ if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+ $contents = "/*<![CDATA[*/$contents/*]]>*/";
+ }
+ return self::rawElement( 'script', $attrs, $contents );
+ }
+
+ /**
+ * Output a <script> tag linking to the given URL, e.g.,
+ * <script src=foo.js></script>.
+ *
+ * @param $url string
+ * @return string Raw HTML
+ */
+ public static function linkedScript( $url ) {
+ global $wgHtml5, $wgJsMimeType;
+
+ $attrs = array( 'src' => $url );
+ if ( !$wgHtml5 ) {
+ $attrs['type'] = $wgJsMimeType;
+ }
+ return self::element( 'script', $attrs );
+ }
+
+ /**
+ * Output a <style> tag with the given contents for the given media type
+ * (if any). TODO: do some useful escaping as well, like if $contents
+ * contains literal '</style>' (admittedly unlikely).
+ *
+ * @param $contents string CSS
+ * @param $media mixed A media type string, like 'screen'
+ * @return string Raw HTML
+ */
+ public static function inlineStyle( $contents, $media = 'all' ) {
+ global $wgWellFormedXml;
+
+ if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) {
+ $contents = "/*<![CDATA[*/$contents/*]]>*/";
+ }
+ return self::rawElement( 'style', array(
+ 'type' => 'text/css',
+ 'media' => $media,
+ ), $contents );
+ }
+
+ /**
+ * Output a <link rel=stylesheet> linking to the given URL for the given
+ * media type (if any).
+ *
+ * @param $url string
+ * @param $media mixed A media type string, like 'screen'
+ * @return string Raw HTML
+ */
+ public static function linkedStyle( $url, $media = 'all' ) {
+ return self::element( 'link', array(
+ 'rel' => 'stylesheet',
+ 'href' => $url,
+ 'type' => 'text/css',
+ 'media' => $media,
+ ) );
+ }
+
+ /**
+ * Convenience function to produce an <input> element. This supports the
+ * new HTML5 input types and attributes, and will silently strip them if
+ * $wgHtml5 is false.
+ *
+ * @param $name string name attribute
+ * @param $value mixed value attribute
+ * @param $type string type attribute
+ * @param $attribs array Associative array of miscellaneous extra
+ * attributes, passed to Html::element()
+ * @return string Raw HTML
+ */
+ public static function input( $name, $value = '', $type = 'text', $attribs = array() ) {
+ $attribs['type'] = $type;
+ $attribs['value'] = $value;
+ $attribs['name'] = $name;
+
+ return self::element( 'input', $attribs );
+ }
+
+ /**
+ * Convenience function to produce an input element with type=hidden, like
+ * Xml::hidden.
+ *
+ * @param $name string name attribute
+ * @param $value string value attribute
+ * @param $attribs array Associative array of miscellaneous extra
+ * attributes, passed to Html::element()
+ * @return string Raw HTML
+ */
+ public static function hidden( $name, $value, $attribs = array() ) {
+ return self::input( $name, $value, 'hidden', $attribs );
+ }
+
+ /**
+ * Convenience function to produce an <input> element. This supports leaving
+ * out the cols= and rows= which Xml requires and are required by HTML4/XHTML
+ * but not required by HTML5 and will silently set cols="" and rows="" if
+ * $wgHtml5 is false and cols and rows are omitted (HTML4 validates present
+ * but empty cols="" and rows="" as valid).
+ *
+ * @param $name string name attribute
+ * @param $value string value attribute
+ * @param $attribs array Associative array of miscellaneous extra
+ * attributes, passed to Html::element()
+ * @return string Raw HTML
+ */
+ public static function textarea( $name, $value = '', $attribs = array() ) {
+ global $wgHtml5;
+ $attribs['name'] = $name;
+ if ( !$wgHtml5 ) {
+ if ( !isset( $attribs['cols'] ) )
+ $attribs['cols'] = "";
+ if ( !isset( $attribs['rows'] ) )
+ $attribs['rows'] = "";
+ }
+ return self::element( 'textarea', $attribs, $value );
+ }
+}
diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php
index 269d45ff..d5983635 100644
--- a/includes/HttpFunctions.php
+++ b/includes/HttpFunctions.php
@@ -1,9 +1,6 @@
<?php
-
/**
* @defgroup HTTP HTTP
- * @file
- * @ingroup HTTP
*/
/**
@@ -11,111 +8,54 @@
* @ingroup HTTP
*/
class Http {
+ static $httpEngine = false;
/**
- * Simple wrapper for Http::request( 'GET' )
- * @see Http::request()
+ * Perform an HTTP request
+ * @param $method string HTTP method. Usually GET/POST
+ * @param $url string Full URL to act on
+ * @param $options options to pass to HttpRequest object
+ * Possible keys for the array:
+ * timeout Timeout length in seconds
+ * postData An array of key-value pairs or a url-encoded form data
+ * proxy The proxy to use. Will use $wgHTTPProxy (if set) otherwise.
+ * noProxy Override $wgHTTPProxy (if set) and don't use any proxy at all.
+ * sslVerifyHost (curl only) Verify the SSL certificate
+ * caInfo (curl only) Provide CA information
+ * maxRedirects Maximum number of redirects to follow (defaults to 5)
+ * followRedirects Whether to follow redirects (defaults to true)
+ * @returns mixed (bool)false on failure or a string on success
*/
- public static function get( $url, $timeout = 'default', $opts = array() ) {
- return Http::request( "GET", $url, $timeout, $opts );
+ public static function request( $method, $url, $options = array() ) {
+ wfDebug( "HTTP: $method: $url" );
+ $options['method'] = strtoupper( $method );
+ if ( !isset( $options['timeout'] ) ) {
+ $options['timeout'] = 'default';
+ }
+ $req = HttpRequest::factory( $url, $options );
+ $status = $req->execute();
+ if ( $status->isOK() ) {
+ return $req->getContent();
+ } else {
+ return false;
+ }
}
/**
- * Simple wrapper for Http::request( 'POST' )
+ * Simple wrapper for Http::request( 'GET' )
* @see Http::request()
*/
- public static function post( $url, $timeout = 'default', $opts = array() ) {
- return Http::request( "POST", $url, $timeout, $opts );
+ public static function get( $url, $timeout = 'default', $options = array() ) {
+ $options['timeout'] = $timeout;
+ return Http::request( 'GET', $url, $options );
}
/**
- * Get the contents of a file by HTTP
- * @param $method string HTTP method. Usually GET/POST
- * @param $url string Full URL to act on
- * @param $timeout int Seconds to timeout. 'default' falls to $wgHTTPTimeout
- * @param $curlOptions array Optional array of extra params to pass
- * to curl_setopt()
- */
- public static function request( $method, $url, $timeout = 'default', $curlOptions = array() ) {
- global $wgHTTPTimeout, $wgHTTPProxy, $wgTitle;
-
- // Go ahead and set the timeout if not otherwise specified
- if ( $timeout == 'default' ) {
- $timeout = $wgHTTPTimeout;
- }
-
- wfDebug( __METHOD__ . ": $method $url\n" );
- # Use curl if available
- if ( function_exists( 'curl_init' ) ) {
- $c = curl_init( $url );
- if ( self::isLocalURL( $url ) ) {
- curl_setopt( $c, CURLOPT_PROXY, 'localhost:80' );
- } else if ($wgHTTPProxy) {
- curl_setopt($c, CURLOPT_PROXY, $wgHTTPProxy);
- }
-
- curl_setopt( $c, CURLOPT_TIMEOUT, $timeout );
- curl_setopt( $c, CURLOPT_USERAGENT, self :: userAgent() );
- if ( $method == 'POST' ) {
- curl_setopt( $c, CURLOPT_POST, true );
- curl_setopt( $c, CURLOPT_POSTFIELDS, '' );
- }
- else
- curl_setopt( $c, CURLOPT_CUSTOMREQUEST, $method );
-
- # Set the referer to $wgTitle, even in command-line mode
- # This is useful for interwiki transclusion, where the foreign
- # server wants to know what the referring page is.
- # $_SERVER['REQUEST_URI'] gives a less reliable indication of the
- # referring page.
- if ( is_object( $wgTitle ) ) {
- curl_setopt( $c, CURLOPT_REFERER, $wgTitle->getFullURL() );
- }
-
- if ( is_array( $curlOptions ) ) {
- foreach( $curlOptions as $option => $value ) {
- curl_setopt( $c, $option, $value );
- }
- }
-
- ob_start();
- curl_exec( $c );
- $text = ob_get_contents();
- ob_end_clean();
-
- # Don't return the text of error messages, return false on error
- $retcode = curl_getinfo( $c, CURLINFO_HTTP_CODE );
- if ( $retcode != 200 ) {
- wfDebug( __METHOD__ . ": HTTP return code $retcode\n" );
- $text = false;
- }
- # Don't return truncated output
- $errno = curl_errno( $c );
- if ( $errno != CURLE_OK ) {
- $errstr = curl_error( $c );
- wfDebug( __METHOD__ . ": CURL error code $errno: $errstr\n" );
- $text = false;
- }
- curl_close( $c );
- } else {
- # Otherwise use file_get_contents...
- # This doesn't have local fetch capabilities...
-
- $headers = array( "User-Agent: " . self :: userAgent() );
- if( strcasecmp( $method, 'post' ) == 0 ) {
- // Required for HTTP 1.0 POSTs
- $headers[] = "Content-Length: 0";
- }
- $opts = array(
- 'http' => array(
- 'method' => $method,
- 'header' => implode( "\r\n", $headers ),
- 'timeout' => $timeout ) );
- $ctx = stream_context_create($opts);
-
- $text = file_get_contents( $url, false, $ctx );
- }
- return $text;
+ * Simple wrapper for Http::request( 'POST' )
+ * @see Http::request()
+ */
+ public static function post( $url, $options = array() ) {
+ return Http::request( 'POST', $url, $options );
}
/**
@@ -151,12 +91,803 @@ class Http {
}
return false;
}
-
+
/**
- * Return a standard user-agent we can use for external requests.
+ * A standard user-agent we can use for external requests.
+ * @returns string
*/
public static function userAgent() {
global $wgVersion;
return "MediaWiki/$wgVersion";
}
+
+ /**
+ * Checks that the given URI is a valid one
+ * @param $uri Mixed: URI to check for validity
+ * @returns bool
+ */
+ public static function isValidURI( $uri ) {
+ return preg_match(
+ '/(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/',
+ $uri,
+ $matches
+ );
+ }
+}
+
+/**
+ * This wrapper class will call out to curl (if available) or fallback
+ * to regular PHP if necessary for handling internal HTTP requests.
+ */
+class HttpRequest {
+ protected $content;
+ protected $timeout = 'default';
+ protected $headersOnly = null;
+ protected $postData = null;
+ protected $proxy = null;
+ protected $noProxy = false;
+ protected $sslVerifyHost = true;
+ protected $caInfo = null;
+ protected $method = "GET";
+ protected $reqHeaders = array();
+ protected $url;
+ protected $parsedUrl;
+ protected $callback;
+ protected $maxRedirects = 5;
+ protected $followRedirects = true;
+
+ protected $cookieJar;
+
+ protected $headerList = array();
+ protected $respVersion = "0.9";
+ protected $respStatus = "200 Ok";
+ protected $respHeaders = array();
+
+ public $status;
+
+ /**
+ * @param $url string url to use
+ * @param $options array (optional) extra params to pass (see Http::request())
+ */
+ function __construct( $url, $options = array() ) {
+ global $wgHTTPTimeout;
+
+ $this->url = $url;
+ $this->parsedUrl = parse_url( $url );
+
+ if ( !Http::isValidURI( $this->url ) ) {
+ $this->status = Status::newFatal('http-invalid-url');
+ } else {
+ $this->status = Status::newGood( 100 ); // continue
+ }
+
+ if ( isset($options['timeout']) && $options['timeout'] != 'default' ) {
+ $this->timeout = $options['timeout'];
+ } else {
+ $this->timeout = $wgHTTPTimeout;
+ }
+
+ $members = array( "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
+ "method", "followRedirects", "maxRedirects" );
+ foreach ( $members as $o ) {
+ if ( isset($options[$o]) ) {
+ $this->$o = $options[$o];
+ }
+ }
+ }
+
+ /**
+ * Generate a new request object
+ * @see HttpRequest::__construct
+ */
+ public static function factory( $url, $options = null ) {
+ if ( !Http::$httpEngine ) {
+ Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
+ } elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
+ throw new MWException( __METHOD__.': curl (http://php.net/curl) is not installed, but'.
+ ' Http::$httpEngine is set to "curl"' );
+ }
+
+ switch( Http::$httpEngine ) {
+ case 'curl':
+ return new CurlHttpRequest( $url, $options );
+ case 'php':
+ if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
+ throw new MWException( __METHOD__.': allow_url_fopen needs to be enabled for pure PHP'.
+ ' http requests to work. If possible, curl should be used instead. See http://php.net/curl.' );
+ }
+ return new PhpHttpRequest( $url, $options );
+ default:
+ throw new MWException( __METHOD__.': The setting of Http::$httpEngine is not valid.' );
+ }
+ }
+
+ /**
+ * Get the body, or content, of the response to the request
+ * @return string
+ */
+ public function getContent() {
+ return $this->content;
+ }
+
+ /**
+ * Take care of setting up the proxy
+ * (override in subclass)
+ * @return string
+ */
+ public function proxySetup() {
+ global $wgHTTPProxy;
+
+ if ( $this->proxy ) {
+ return;
+ }
+ if ( Http::isLocalURL( $this->url ) ) {
+ $this->proxy = 'http://localhost:80/';
+ } elseif ( $wgHTTPProxy ) {
+ $this->proxy = $wgHTTPProxy ;
+ } elseif ( getenv( "http_proxy" ) ) {
+ $this->proxy = getenv( "http_proxy" );
+ }
+ }
+
+ /**
+ * Set the refererer header
+ */
+ public function setReferer( $url ) {
+ $this->setHeader('Referer', $url);
+ }
+
+ /**
+ * Set the user agent
+ */
+ public function setUserAgent( $UA ) {
+ $this->setHeader('User-Agent', $UA);
+ }
+
+ /**
+ * Set an arbitrary header
+ */
+ public function setHeader($name, $value) {
+ // I feel like I should normalize the case here...
+ $this->reqHeaders[$name] = $value;
+ }
+
+ /**
+ * Get an array of the headers
+ */
+ public function getHeaderList() {
+ $list = array();
+
+ if( $this->cookieJar ) {
+ $this->reqHeaders['Cookie'] =
+ $this->cookieJar->serializeToHttpRequest($this->parsedUrl['path'],
+ $this->parsedUrl['host']);
+ }
+ foreach($this->reqHeaders as $name => $value) {
+ $list[] = "$name: $value";
+ }
+ return $list;
+ }
+
+ /**
+ * Set the callback
+ * @param $callback callback
+ */
+ public function setCallback( $callback ) {
+ $this->callback = $callback;
+ }
+
+ /**
+ * A generic callback to read the body of the response from a remote
+ * server.
+ * @param $fh handle
+ * @param $content string
+ */
+ public function read( $fh, $content ) {
+ $this->content .= $content;
+ return strlen( $content );
+ }
+
+ /**
+ * Take care of whatever is necessary to perform the URI request.
+ * @return Status
+ */
+ public function execute() {
+ global $wgTitle;
+
+ if( strtoupper($this->method) == "HEAD" ) {
+ $this->headersOnly = true;
+ }
+
+ if ( is_array( $this->postData ) ) {
+ $this->postData = wfArrayToCGI( $this->postData );
+ }
+
+ if ( is_object( $wgTitle ) && !isset($this->reqHeaders['Referer']) ) {
+ $this->setReferer( $wgTitle->getFullURL() );
+ }
+
+ if ( !$this->noProxy ) {
+ $this->proxySetup();
+ }
+
+ if ( !$this->callback ) {
+ $this->setCallback( array( $this, 'read' ) );
+ }
+
+ if ( !isset($this->reqHeaders['User-Agent']) ) {
+ $this->setUserAgent(Http::userAgent());
+ }
+ }
+
+ /**
+ * Parses the headers, including the HTTP status code and any
+ * Set-Cookie headers. This function expectes the headers to be
+ * found in an array in the member variable headerList.
+ * @returns nothing
+ */
+ protected function parseHeader() {
+ $lastname = "";
+ foreach( $this->headerList as $header ) {
+ if( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
+ $this->respVersion = $match[1];
+ $this->respStatus = $match[2];
+ } elseif( preg_match( "#^[ \t]#", $header ) ) {
+ $last = count($this->respHeaders[$lastname]) - 1;
+ $this->respHeaders[$lastname][$last] .= "\r\n$header";
+ } elseif( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
+ $this->respHeaders[strtolower( $match[1] )][] = $match[2];
+ $lastname = strtolower( $match[1] );
+ }
+ }
+
+ $this->parseCookies();
+ }
+
+ /**
+ * Sets the member variable status to a fatal status if the HTTP
+ * status code was not 200.
+ * @returns nothing
+ */
+ protected function setStatus() {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+
+ if((int)$this->respStatus !== 200) {
+ list( $code, $message ) = explode(" ", $this->respStatus, 2);
+ $this->status->fatal("http-bad-status", $code, $message );
+ }
+ }
+
+
+ /**
+ * Returns true if the last status code was a redirect.
+ * @return bool
+ */
+ public function isRedirect() {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+
+ $status = (int)$this->respStatus;
+ if ( $status >= 300 && $status < 400 ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns an associative array of response headers after the
+ * request has been executed. Because some headers
+ * (e.g. Set-Cookie) can appear more than once the, each value of
+ * the associative array is an array of the values given.
+ * @return array
+ */
+ public function getResponseHeaders() {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+ return $this->respHeaders;
+ }
+
+ /**
+ * Returns the value of the given response header.
+ * @param $header string
+ * @return string
+ */
+ public function getResponseHeader($header) {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+ if ( isset( $this->respHeaders[strtolower ( $header ) ] ) ) {
+ $v = $this->respHeaders[strtolower ( $header ) ];
+ return $v[count( $v ) - 1];
+ }
+ return null;
+ }
+
+ /**
+ * Tells the HttpRequest object to use this pre-loaded CookieJar.
+ * @param $jar CookieJar
+ */
+ public function setCookieJar( $jar ) {
+ $this->cookieJar = $jar;
+ }
+
+ /**
+ * Returns the cookie jar in use.
+ * @returns CookieJar
+ */
+ public function getCookieJar() {
+ if( !$this->respHeaders ) {
+ $this->parseHeader();
+ }
+ return $this->cookieJar;
+ }
+
+ /**
+ * Sets a cookie. Used before a request to set up any individual
+ * cookies. Used internally after a request to parse the
+ * Set-Cookie headers.
+ * @see Cookie::set
+ */
+ public function setCookie( $name, $value = null, $attr = null) {
+ if( !$this->cookieJar ) {
+ $this->cookieJar = new CookieJar;
+ }
+ $this->cookieJar->setCookie($name, $value, $attr);
+ }
+
+ /**
+ * Parse the cookies in the response headers and store them in the cookie jar.
+ */
+ protected function parseCookies() {
+ if( !$this->cookieJar ) {
+ $this->cookieJar = new CookieJar;
+ }
+ if( isset( $this->respHeaders['set-cookie'] ) ) {
+ $url = parse_url( $this->getFinalUrl() );
+ foreach( $this->respHeaders['set-cookie'] as $cookie ) {
+ $this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
+ }
+ }
+ }
+
+ /**
+ * Returns the final URL after all redirections.
+ * @returns string
+ */
+ public function getFinalUrl() {
+ $location = $this->getResponseHeader("Location");
+ if ( $location ) {
+ return $location;
+ }
+
+ return $this->url;
+ }
+}
+
+
+class Cookie {
+ protected $name;
+ protected $value;
+ protected $expires;
+ protected $path;
+ protected $domain;
+ protected $isSessionKey = true;
+ // TO IMPLEMENT protected $secure
+ // TO IMPLEMENT? protected $maxAge (add onto expires)
+ // TO IMPLEMENT? protected $version
+ // TO IMPLEMENT? protected $comment
+
+ function __construct( $name, $value, $attr ) {
+ $this->name = $name;
+ $this->set( $value, $attr );
+ }
+
+ /**
+ * Sets a cookie. Used before a request to set up any individual
+ * cookies. Used internally after a request to parse the
+ * Set-Cookie headers.
+ * @param $name string the name of the cookie
+ * @param $value string the value of the cookie
+ * @param $attr array possible key/values:
+ * expires A date string
+ * path The path this cookie is used on
+ * domain Domain this cookie is used on
+ */
+ public function set( $value, $attr ) {
+ $this->value = $value;
+ if( isset( $attr['expires'] ) ) {
+ $this->isSessionKey = false;
+ $this->expires = strtotime( $attr['expires'] );
+ }
+ if( isset( $attr['path'] ) ) {
+ $this->path = $attr['path'];
+ } else {
+ $this->path = "/";
+ }
+ if( isset( $attr['domain'] ) ) {
+ if( self::validateCookieDomain( $attr['domain'] ) ) {
+ $this->domain = $attr['domain'];
+ }
+ } else {
+ throw new MWException("You must specify a domain.");
+ }
+ }
+
+ /**
+ * Return the true if the cookie is valid is valid. Otherwise,
+ * false. The uses a method similar to IE cookie security
+ * described here:
+ * http://kuza55.blogspot.com/2008/02/understanding-cookie-security.html
+ * A better method might be to use a blacklist like
+ * http://publicsuffix.org/
+ *
+ * @param $domain string the domain to validate
+ * @param $originDomain string (optional) the domain the cookie originates from
+ * @return bool
+ */
+ public static function validateCookieDomain( $domain, $originDomain = null) {
+ // Don't allow a trailing dot
+ if( substr( $domain, -1 ) == "." ) return false;
+
+ $dc = explode(".", $domain);
+
+ // Don't allow cookies for "localhost", "ls" or other dot-less hosts
+ if( count($dc) < 2 ) return false;
+
+ // Only allow full, valid IP addresses
+ if( preg_match( '/^[0-9.]+$/', $domain ) ) {
+ if( count( $dc ) != 4 ) return false;
+
+ if( ip2long( $domain ) === false ) return false;
+
+ if( $originDomain == null || $originDomain == $domain ) return true;
+
+ }
+
+ // Don't allow cookies for "co.uk" or "gov.uk", etc, but allow "supermarket.uk"
+ if( strrpos( $domain, "." ) - strlen( $domain ) == -3 ) {
+ if( (count($dc) == 2 && strlen( $dc[0] ) <= 2 )
+ || (count($dc) == 3 && strlen( $dc[0] ) == "" && strlen( $dc[1] ) <= 2 ) ) {
+ return false;
+ }
+ if( (count($dc) == 2 || (count($dc) == 3 && $dc[0] == "") )
+ && preg_match( '/(com|net|org|gov|edu)\...$/', $domain) ) {
+ return false;
+ }
+ }
+
+ if( $originDomain != null ) {
+ if( substr( $domain, 0, 1 ) != "." && $domain != $originDomain ) {
+ return false;
+ }
+ if( substr( $domain, 0, 1 ) == "."
+ && substr_compare( $originDomain, $domain, -strlen( $domain ),
+ strlen( $domain ), TRUE ) != 0 ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Serialize the cookie jar into a format useful for HTTP Request headers.
+ * @param $path string the path that will be used. Required.
+ * @param $domain string the domain that will be used. Required.
+ * @return string
+ */
+ public function serializeToHttpRequest( $path, $domain ) {
+ $ret = "";
+
+ if( $this->canServeDomain( $domain )
+ && $this->canServePath( $path )
+ && $this->isUnExpired() ) {
+ $ret = $this->name ."=". $this->value;
+ }
+
+ return $ret;
+ }
+
+ protected function canServeDomain( $domain ) {
+ if( $domain == $this->domain
+ || ( strlen( $domain) > strlen( $this->domain )
+ && substr( $this->domain, 0, 1) == "."
+ && substr_compare( $domain, $this->domain, -strlen( $this->domain ),
+ strlen( $this->domain ), TRUE ) == 0 ) ) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function canServePath( $path ) {
+ if( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ) {
+ return true;
+ }
+ return false;
+ }
+
+ protected function isUnExpired() {
+ if( $this->isSessionKey || $this->expires > time() ) {
+ return true;
+ }
+ return false;
+ }
+
+}
+
+class CookieJar {
+ private $cookie = array();
+
+ /**
+ * Set a cookie in the cookie jar. Make sure only one cookie per-name exists.
+ * @see Cookie::set()
+ */
+ public function setCookie ($name, $value, $attr) {
+ /* cookies: case insensitive, so this should work.
+ * We'll still send the cookies back in the same case we got them, though.
+ */
+ $index = strtoupper($name);
+ if( isset( $this->cookie[$index] ) ) {
+ $this->cookie[$index]->set( $value, $attr );
+ } else {
+ $this->cookie[$index] = new Cookie( $name, $value, $attr );
+ }
+ }
+
+ /**
+ * @see Cookie::serializeToHttpRequest
+ */
+ public function serializeToHttpRequest( $path, $domain ) {
+ $cookies = array();
+
+ foreach( $this->cookie as $c ) {
+ $serialized = $c->serializeToHttpRequest( $path, $domain );
+ if ( $serialized ) $cookies[] = $serialized;
+ }
+
+ return implode("; ", $cookies);
+ }
+
+ /**
+ * Parse the content of an Set-Cookie HTTP Response header.
+ * @param $cookie string
+ */
+ public function parseCookieResponseHeader ( $cookie, $domain ) {
+ $len = strlen( "Set-Cookie:" );
+ if ( substr_compare( "Set-Cookie:", $cookie, 0, $len, TRUE ) === 0 ) {
+ $cookie = substr( $cookie, $len );
+ }
+
+ $bit = array_map( 'trim', explode( ";", $cookie ) );
+ if ( count($bit) >= 1 ) {
+ list($name, $value) = explode( "=", array_shift( $bit ), 2 );
+ $attr = array();
+ foreach( $bit as $piece ) {
+ $parts = explode( "=", $piece );
+ if( count( $parts ) > 1 ) {
+ $attr[strtolower( $parts[0] )] = $parts[1];
+ } else {
+ $attr[strtolower( $parts[0] )] = true;
+ }
+ }
+
+ if( !isset( $attr['domain'] ) ) {
+ $attr['domain'] = $domain;
+ } elseif ( !Cookie::validateCookieDomain( $attr['domain'], $domain ) ) {
+ return null;
+ }
+
+ $this->setCookie( $name, $value, $attr );
+ }
+ }
+}
+
+
+/**
+ * HttpRequest implemented using internal curl compiled into PHP
+ */
+class CurlHttpRequest extends HttpRequest {
+ static $curlMessageMap = array(
+ 6 => 'http-host-unreachable',
+ 28 => 'http-timed-out'
+ );
+
+ protected $curlOptions = array();
+ protected $headerText = "";
+
+ protected function readHeader( $fh, $content ) {
+ $this->headerText .= $content;
+ return strlen( $content );
+ }
+
+ public function execute() {
+ parent::execute();
+ if ( !$this->status->isOK() ) {
+ return $this->status;
+ }
+ $this->curlOptions[CURLOPT_PROXY] = $this->proxy;
+ $this->curlOptions[CURLOPT_TIMEOUT] = $this->timeout;
+ $this->curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
+ $this->curlOptions[CURLOPT_WRITEFUNCTION] = $this->callback;
+ $this->curlOptions[CURLOPT_HEADERFUNCTION] = array($this, "readHeader");
+ $this->curlOptions[CURLOPT_MAXREDIRS] = $this->maxRedirects;
+
+ /* not sure these two are actually necessary */
+ if(isset($this->reqHeaders['Referer'])) {
+ $this->curlOptions[CURLOPT_REFERER] = $this->reqHeaders['Referer'];
+ }
+ $this->curlOptions[CURLOPT_USERAGENT] = $this->reqHeaders['User-Agent'];
+
+ if ( $this->sslVerifyHost ) {
+ $this->curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->sslVerifyHost;
+ }
+
+ if ( $this->caInfo ) {
+ $this->curlOptions[CURLOPT_CAINFO] = $this->caInfo;
+ }
+
+ if ( $this->headersOnly ) {
+ $this->curlOptions[CURLOPT_NOBODY] = true;
+ $this->curlOptions[CURLOPT_HEADER] = true;
+ } elseif ( $this->method == 'POST' ) {
+ $this->curlOptions[CURLOPT_POST] = true;
+ $this->curlOptions[CURLOPT_POSTFIELDS] = $this->postData;
+ // Suppress 'Expect: 100-continue' header, as some servers
+ // will reject it with a 417 and Curl won't auto retry
+ // with HTTP 1.0 fallback
+ $this->reqHeaders['Expect'] = '';
+ } else {
+ $this->curlOptions[CURLOPT_CUSTOMREQUEST] = $this->method;
+ }
+
+ $this->curlOptions[CURLOPT_HTTPHEADER] = $this->getHeaderList();
+
+ $curlHandle = curl_init( $this->url );
+ if ( !curl_setopt_array( $curlHandle, $this->curlOptions ) ) {
+ throw new MWException("Error setting curl options.");
+ }
+ if ( ! @curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, $this->followRedirects ) ) {
+ wfDebug("Couldn't set CURLOPT_FOLLOWLOCATION. Probably safe_mode or open_basedir is set.");
+ /* Continue the processing. If it were in curl_setopt_array, processing would have halted on its entry */
+ }
+
+ if ( false === curl_exec( $curlHandle ) ) {
+ $code = curl_error( $curlHandle );
+
+ if ( isset( self::$curlMessageMap[$code] ) ) {
+ $this->status->fatal( self::$curlMessageMap[$code] );
+ } else {
+ $this->status->fatal( 'http-curl-error', curl_error( $curlHandle ) );
+ }
+ } else {
+ $this->headerList = explode("\r\n", $this->headerText);
+ }
+
+ curl_close( $curlHandle );
+
+ $this->parseHeader();
+ $this->setStatus();
+ return $this->status;
+ }
+}
+
+class PhpHttpRequest extends HttpRequest {
+ protected $manuallyRedirect = false;
+
+ protected function urlToTcp( $url ) {
+ $parsedUrl = parse_url( $url );
+
+ return 'tcp://' . $parsedUrl['host'] . ':' . $parsedUrl['port'];
+ }
+
+ public function execute() {
+ parent::execute();
+
+ // At least on Centos 4.8 with PHP 5.1.6, using max_redirects to follow redirects
+ // causes a segfault
+ if ( version_compare( '5.1.7', phpversion(), '>' ) ) {
+ $this->manuallyRedirect = true;
+ }
+
+ if ( $this->parsedUrl['scheme'] != 'http' ) {
+ $this->status->fatal( 'http-invalid-scheme', $this->parsedUrl['scheme'] );
+ }
+
+ $this->reqHeaders['Accept'] = "*/*";
+ if ( $this->method == 'POST' ) {
+ // Required for HTTP 1.0 POSTs
+ $this->reqHeaders['Content-Length'] = strlen( $this->postData );
+ $this->reqHeaders['Content-type'] = "application/x-www-form-urlencoded";
+ }
+
+ $options = array();
+ if ( $this->proxy && !$this->noProxy ) {
+ $options['proxy'] = $this->urlToTCP( $this->proxy );
+ $options['request_fulluri'] = true;
+ }
+
+ if ( !$this->followRedirects || $this->manuallyRedirect ) {
+ $options['max_redirects'] = 0;
+ } else {
+ $options['max_redirects'] = $this->maxRedirects;
+ }
+
+ $options['method'] = $this->method;
+ $options['header'] = implode("\r\n", $this->getHeaderList());
+ // Note that at some future point we may want to support
+ // HTTP/1.1, but we'd have to write support for chunking
+ // in version of PHP < 5.3.1
+ $options['protocol_version'] = "1.0";
+
+ // This is how we tell PHP we want to deal with 404s (for example) ourselves.
+ // Only works on 5.2.10+
+ $options['ignore_errors'] = true;
+
+ if ( $this->postData ) {
+ $options['content'] = $this->postData;
+ }
+
+ $oldTimeout = false;
+ if ( version_compare( '5.2.1', phpversion(), '>' ) ) {
+ $oldTimeout = ini_set('default_socket_timeout', $this->timeout);
+ } else {
+ $options['timeout'] = $this->timeout;
+ }
+
+ $context = stream_context_create( array( 'http' => $options ) );
+
+ $this->headerList = array();
+ $reqCount = 0;
+ $url = $this->url;
+ do {
+ $again = false;
+ $reqCount++;
+ wfSuppressWarnings();
+ $fh = fopen( $url, "r", false, $context );
+ wfRestoreWarnings();
+ if ( $fh ) {
+ $result = stream_get_meta_data( $fh );
+ $this->headerList = $result['wrapper_data'];
+ $this->parseHeader();
+ $url = $this->getResponseHeader("Location");
+ $again = $this->manuallyRedirect && $this->followRedirects && $url
+ && $this->isRedirect() && $this->maxRedirects > $reqCount;
+ }
+ } while ( $again );
+
+ if ( $oldTimeout !== false ) {
+ ini_set('default_socket_timeout', $oldTimeout);
+ }
+ $this->setStatus();
+
+ if ( $fh === false ) {
+ $this->status->fatal( 'http-request-error' );
+ return $this->status;
+ }
+
+ if ( $result['timed_out'] ) {
+ $this->status->fatal( 'http-timed-out', $this->url );
+ return $this->status;
+ }
+
+ if($this->status->isOK()) {
+ while ( !feof( $fh ) ) {
+ $buf = fread( $fh, 8192 );
+ if ( $buf === false ) {
+ $this->status->fatal( 'http-read-error' );
+ break;
+ }
+ if ( strlen( $buf ) ) {
+ call_user_func( $this->callback, $fh, $buf );
+ }
+ }
+ }
+ fclose( $fh );
+
+ return $this->status;
+ }
}
diff --git a/includes/IP.php b/includes/IP.php
index e5973c2b..bbe70339 100644
--- a/includes/IP.php
+++ b/includes/IP.php
@@ -18,16 +18,24 @@ define( 'RE_IPV6_GAP', ':(?:0+:)*(?::(?:0+:)*)?' );
define( 'RE_IPV6_V4_PREFIX', '0*' . RE_IPV6_GAP . '(?:ffff:)?' );
// An IPv6 block is an IP address and a prefix (d1 to d128)
define( 'RE_IPV6_PREFIX', '(12[0-8]|1[01][0-9]|[1-9]?\d)');
-// An IPv6 IP is made up of 8 octets. However abbreviations like "::" can be used. This is lax!
-define( 'RE_IPV6_ADD', '(:(:' . RE_IPV6_WORD . '){1,7}|' . RE_IPV6_WORD . '(:{1,2}' . RE_IPV6_WORD . '|::$){1,7})' );
+// An IPv6 IP is made up of 8 octets. However abbreviations like "::" can be used.
+// This is lax! Number of octets/double colons validation not done.
+define( 'RE_IPV6_ADD',
+ '(' .
+ ':(:' . RE_IPV6_WORD . '){1,7}' . // IPs that start with ":"
+ '|' .
+ RE_IPV6_WORD . '(:{1,2}' . RE_IPV6_WORD . '|::$){1,7}' . // IPs that don't start with ":"
+ ')'
+);
define( 'RE_IPV6_BLOCK', RE_IPV6_ADD . '\/' . RE_IPV6_PREFIX );
// This might be useful for regexps used elsewhere, matches any IPv6 or IPv6 address or network
define( 'IP_ADDRESS_STRING',
'(?:' .
- RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)' .
+ RE_IP_ADD . '(\/' . RE_IP_PREFIX . '|)' . // IPv4
'|' .
- RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)' .
- ')' );
+ RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)' . // IPv6
+ ')'
+);
/**
* A collection of public static functions to play with IP address
@@ -52,10 +60,12 @@ class IP {
public static function isIPv6( $ip ) {
if ( !$ip ) return false;
if( is_array( $ip ) ) {
- throw new MWException( "invalid value passed to " . __METHOD__ );
+ throw new MWException( "invalid value passed to " . __METHOD__ );
}
+ $doubleColons = substr_count($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);
+ return preg_match( '/^' . RE_IPV6_ADD . '(\/' . RE_IPV6_PREFIX . '|)$/', $ip)
+ && ( $doubleColons == 1 || substr_count($ip,':') == 7 );
}
public static function isIPv4( $ip ) {
@@ -98,13 +108,13 @@ class IP {
*/
public static function toUnsigned6( $ip ) {
if ( !$ip ) return null;
- $ip = explode(':', self::sanitizeIP( $ip ) );
- $r_ip = '';
- foreach ($ip as $v) {
- $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
- }
- $r_ip = wfBaseConvert( $r_ip, 16, 10 );
- return $r_ip;
+ $ip = explode(':', self::sanitizeIP( $ip ) );
+ $r_ip = '';
+ foreach ($ip as $v) {
+ $r_ip .= str_pad( $v, 4, 0, STR_PAD_LEFT );
+ }
+ $r_ip = wfBaseConvert( $r_ip, 16, 10 );
+ return $r_ip;
}
/**
@@ -123,14 +133,23 @@ class IP {
// Remove any whitespaces, convert to upper case
$ip = strtoupper( $ip );
// Expand zero abbreviations
- if ( strpos( $ip, '::' ) !== false ) {
- $ip = str_replace('::', str_repeat(':0', 8 - substr_count($ip, ':')) . ':', $ip);
- }
- // For IPs that start with "::", correct the final IP so that it starts with '0' and not ':'
- if ( $ip[0] == ':' ) $ip = "0$ip";
- // Remove leading zereos from each bloc as needed
- $ip = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip );
- return $ip;
+ $abbrevPos = strpos( $ip, '::' );
+ if ( $abbrevPos !== false ) {
+ // If the '::' is at the beginning...
+ if( $abbrevPos == 0 ) {
+ $repeat = '0:'; $extra = ''; $pad = 9; // 7+2 (due to '::')
+ // If the '::' is at the end...
+ } else if( $abbrevPos == (strlen($ip)-2) ) {
+ $repeat = ':0'; $extra = ''; $pad = 9; // 7+2 (due to '::')
+ // If the '::' is at the end...
+ } else {
+ $repeat = ':0'; $extra = ':'; $pad = 8; // 6+2 (due to '::')
+ }
+ $ip = str_replace('::', str_repeat($repeat, $pad-substr_count($ip,':')).$extra, $ip);
+ }
+ // Remove leading zereos from each bloc as needed
+ $ip = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip );
+ return $ip;
}
/**
@@ -148,7 +167,18 @@ class IP {
}
// NO leading zeroes
$ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct );
- return $ip_oct;
+ return $ip_oct;
+ }
+
+ /**
+ * Convert an IPv4 or IPv6 hexadecimal representation back to readable format
+ */
+ public static function formatHex( $hex ) {
+ if ( substr( $hex, 0, 3 ) == 'v6-' ) {
+ return self::hexToOctet( $hex );
+ } else {
+ return self::hexToQuad( $hex );
+ }
}
/**
@@ -156,7 +186,7 @@ class IP {
* @param $ip string hex IP
* @return string
*/
- public static function HextoOctet( $ip_hex ) {
+ public static function hextoOctet( $ip_hex ) {
// Convert to padded uppercase hex
$ip_hex = str_pad( strtoupper($ip_hex), 32, '0');
// Separate into 8 octets
@@ -166,7 +196,7 @@ class IP {
}
// NO leading zeroes
$ip_oct = preg_replace( '/(^|:)0+' . RE_IPV6_WORD . '/', '$1$2', $ip_oct );
- return $ip_oct;
+ return $ip_oct;
}
/**
@@ -176,14 +206,14 @@ class IP {
*/
public static function hexToQuad( $ip ) {
// Converts a hexadecimal IP to nnn.nnn.nnn.nnn format
- $dec = wfBaseConvert( $ip, 16, 10 );
- $parts[3] = $dec % 256;
- $dec /= 256;
- $parts[2] = $dec % 256;
- $dec /= 256;
- $parts[1] = $dec % 256;
- $parts[0] = $dec / 256;
- return implode( '.', array_reverse( $parts ) );
+ $s = '';
+ for ( $i = 0; $i < 4; $i++ ) {
+ if ( $s !== '' ) {
+ $s .= '.';
+ }
+ $s .= base_convert( substr( $ip, $i * 2, 2 ), 16, 10 );
+ }
+ return $s;
}
/**
@@ -267,7 +297,7 @@ class IP {
} else {
return array( $start, $end );
}
- }
+ }
/**
* Validate an IP address.
@@ -481,40 +511,40 @@ class IP {
} else {
return array( $start, $end );
}
- }
+ }
- /**
- * Determine if a given IPv4/IPv6 address is in a given CIDR network
- * @param $addr The address to check against the given range.
- * @param $range The range to check the given address against.
- * @return bool Whether or not the given address is in the given range.
- */
- public static function isInRange( $addr, $range ) {
- // Convert to IPv6 if needed
- $unsignedIP = self::toHex( $addr );
- list( $start, $end ) = self::parseRange( $range );
- return (($unsignedIP >= $start) && ($unsignedIP <= $end));
- }
+ /**
+ * Determine if a given IPv4/IPv6 address is in a given CIDR network
+ * @param $addr The address to check against the given range.
+ * @param $range The range to check the given address against.
+ * @return bool Whether or not the given address is in the given range.
+ */
+ public static function isInRange( $addr, $range ) {
+ // Convert to IPv6 if needed
+ $hexIP = self::toHex( $addr );
+ list( $start, $end ) = self::parseRange( $range );
+ return (strcmp($hexIP, $start) >= 0 &&
+ strcmp($hexIP, $end) <= 0);
+ }
- /**
- * Convert some unusual representations of IPv4 addresses to their
- * canonical dotted quad representation.
- *
- * This currently only checks a few IPV4-to-IPv6 related cases. More
- * unusual representations may be added later.
- *
- * @param $addr something that might be an IP address
- * @return valid dotted quad IPv4 address or null
- */
- public static function canonicalize( $addr ) {
+ /**
+ * Convert some unusual representations of IPv4 addresses to their
+ * canonical dotted quad representation.
+ *
+ * This currently only checks a few IPV4-to-IPv6 related cases. More
+ * unusual representations may be added later.
+ *
+ * @param $addr something that might be an IP address
+ * @return valid dotted quad IPv4 address or null
+ */
+ public static function canonicalize( $addr ) {
if ( self::isValid( $addr ) )
return $addr;
- // Annoying IPv6 representations like ::ffff:1.2.3.4
+ // Turn mapped addresses from ::ce:ffff:1.2.3.4 to 1.2.3.4
if ( strpos($addr,':') !==false && strpos($addr,'.') !==false ) {
- $addr = str_replace( '.', ':', $addr );
- if( IP::isIPv6( $addr ) )
- return $addr;
+ $addr = substr( $addr, strrpos($addr,':')+1 );
+ if( self::isIPv4($addr) ) return $addr;
}
// IPv6 loopback address
@@ -524,10 +554,10 @@ class IP {
// IPv4-mapped and IPv4-compatible IPv6 addresses
if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . '(' . RE_IP_ADD . ')$/i', $addr, $m ) )
- return $m[1];
+ return $m[1];
if ( preg_match( '/^' . RE_IPV6_V4_PREFIX . RE_IPV6_WORD . ':' . RE_IPV6_WORD . '$/i', $addr, $m ) )
- return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
+ return long2ip( ( hexdec( $m[1] ) << 16 ) + hexdec( $m[2] ) );
return null; // give up
- }
+ }
}
diff --git a/includes/ImageFunctions.php b/includes/ImageFunctions.php
index 73d935a7..5f01ab6e 100644
--- a/includes/ImageFunctions.php
+++ b/includes/ImageFunctions.php
@@ -123,6 +123,12 @@ function wfIsBadImage( $name, $contextTitle = false ) {
static $badImages = false;
wfProfileIn( __METHOD__ );
+ # Handle redirects
+ $redirectTitle = RepoGroup::singleton()->checkRedirect( Title::makeTitle( NS_FILE, $name ) );
+ if( $redirectTitle ) {
+ $name = $redirectTitle->getDbKey();
+ }
+
# Run the extension hook
$bad = false;
if( !wfRunHooks( 'BadImage', array( $name, &$bad ) ) ) {
diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php
index 8a38bed7..5bff0ae3 100644
--- a/includes/ImageGallery.php
+++ b/includes/ImageGallery.php
@@ -236,13 +236,13 @@ class ImageGallery
$i = 0;
foreach ( $this->mImages as $pair ) {
$nt = $pair[0];
- $text = $pair[1];
+ $text = $pair[1]; # "text" means "caption" here
# Give extensions a chance to select the file revision for us
$time = $descQuery = false;
wfRunHooks( 'BeforeGalleryFindFile', array( &$this, &$nt, &$time, &$descQuery ) );
- $img = wfFindFile( $nt, $time );
+ $img = wfFindFile( $nt, array( 'time' => $time ) );
if( $nt->getNamespace() != NS_FILE || !$img ) {
# We're dealing with a non-image, spit out the name and be done with it.
@@ -250,14 +250,30 @@ class ImageGallery
. htmlspecialchars( $nt->getText() ) . '</div>';
} elseif( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) {
# The image is blacklisted, just show it as a text link.
- $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">'
- . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '</div>';
+ $thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">' .
+ $sk->link(
+ $nt,
+ htmlspecialchars( $nt->getText() ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) .
+ '</div>';
} elseif( !( $thumb = $img->transform( $params ) ) ) {
# Error generating thumbnail.
$thumbhtml = "\n\t\t\t".'<div style="height: '.($this->mHeights*1.25+2).'px;">'
. htmlspecialchars( $img->getLastError() ) . '</div>';
} else {
$vpad = floor( ( 1.25*$this->mHeights - $thumb->height ) /2 ) - 2;
+
+ $imageParameters = array(
+ 'desc-link' => true,
+ 'desc-query' => $descQuery
+ );
+ # In the absence of a caption, fall back on providing screen readers with the filename as alt text
+ if ( $text == '' ) {
+ $imageParameters['alt'] = $nt->getText();
+ }
$thumbhtml = "\n\t\t\t".
'<div class="thumb" style="padding: ' . $vpad . 'px 0; width: ' .($this->mWidths+30).'px;">'
@@ -265,7 +281,7 @@ class ImageGallery
# 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, 'desc-query' => $descQuery ) ) . '</div></div>';
+ . $thumb->toHtml( $imageParameters ) . '</div></div>';
// Call parser transform hook
if ( $this->mParser && $img->getHandler() ) {
@@ -274,7 +290,8 @@ class ImageGallery
}
//TODO
- //$ul = $sk->makeLink( $wgContLang->getNsText( MWNamespace::getUser() ) . ":{$ut}", $ut );
+ // $linkTarget = Title::newFromText( $wgContLang->getNsText( MWNamespace::getUser() ) . ":{$ut}" );
+ // $ul = $sk->link( $linkTarget, $ut );
if( $this->mShowBytes ) {
if( $img ) {
@@ -289,7 +306,13 @@ class ImageGallery
}
$textlink = $this->mShowFilename ?
- $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20 ) ) ) . "<br />\n" :
+ $sk->link(
+ $nt,
+ htmlspecialchars( $wgLang->truncate( $nt->getText(), 20 ) ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) . "<br />\n" :
'' ;
# ATTENTION: The newline after <div class="gallerytext"> is needed to accommodate htmltidy which
diff --git a/includes/ImagePage.php b/includes/ImagePage.php
index 4f3b859a..dd2c2ab1 100644
--- a/includes/ImagePage.php
+++ b/includes/ImagePage.php
@@ -84,6 +84,8 @@ class ImagePage extends Article {
if( $this->mTitle->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) )
return Article::view();
+
+ $this->showRedirectedFromHeader();
if( $wgShowEXIF && $this->displayImg->exists() ) {
// FIXME: bad interface, see note on MediaHandler::formatMetadata().
@@ -115,7 +117,7 @@ class ImagePage extends Article {
if( $fol != '-' && !wfEmptyMsg( 'shareddescriptionfollows', $fol ) ) {
$wgOut->addWikiText( $fol );
}
- $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' );
+ $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
}
$this->closeShowImage();
@@ -130,12 +132,18 @@ class ImagePage extends Article {
# Yet we return metadata about the target. Definitely an issue in the FileRepo
$this->imageRedirects();
$this->imageLinks();
+
+ # Allow extensions to add something after the image links
+ $html = '';
+ wfRunHooks( 'ImagePageAfterImageLinks', array( $this, &$html ) );
+ if ( $html)
+ $wgOut->addHTML( $html );
if( $showmeta ) {
global $wgStylePath, $wgStyleVersion;
$expand = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-expand' ) ) );
$collapse = htmlspecialchars( Xml::escapeJsString( wfMsg( 'metadata-collapse' ) ) );
- $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ). "\n" );
+ $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ) . "\n" );
$wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
$wgOut->addScriptFile( 'metadata.js' );
$wgOut->addHTML(
@@ -222,14 +230,18 @@ class ImagePage extends Article {
* @return string
*/
protected function showTOC( $metadata ) {
- global $wgLang;
- $r = '<ul id="filetoc">
- <li><a href="#file">' . $wgLang->getNsText( NS_FILE ) . '</a></li>
- <li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li>
- <li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>' .
- ($metadata ? ' <li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>' : '') . '
- </ul>';
- return $r;
+ $r = array(
+ '<li><a href="#file">' . wfMsgHtml( 'file-anchor-link' ) . '</a></li>',
+ '<li><a href="#filehistory">' . wfMsgHtml( 'filehist' ) . '</a></li>',
+ '<li><a href="#filelinks">' . wfMsgHtml( 'imagelinks' ) . '</a></li>',
+ );
+ if ( $metadata ) {
+ $r[] = '<li><a href="#metadata">' . wfMsgHtml( 'metadata' ) . '</a></li>';
+ }
+
+ wfRunHooks( 'ImagePageShowTOC', array( $this, &$r ) );
+
+ return '<ul id="filetoc">' . implode( "\n", $r ) . '</ul>';
}
/**
@@ -241,8 +253,9 @@ class ImagePage extends Article {
* @return string
*/
protected function makeMetadataTable( $metadata ) {
- $r = wfMsg( 'metadata-help' ) . "\n\n";
- $r .= "{| id=mw_metadata class=mw_metadata\n";
+ $r = "<div class=\"mw-imagepage-section-metadata\">";
+ $r .= wfMsgNoTrans( 'metadata-help' );
+ $r .= "<table id=\"mw_metadata\" class=\"mw_metadata\">\n";
foreach ( $metadata as $type => $stuff ) {
foreach ( $stuff as $v ) {
# FIXME, why is this using escapeId for a class?!
@@ -250,12 +263,12 @@ class ImagePage extends Article {
if( $type == 'collapsed' ) {
$class .= ' collapsable';
}
- $r .= "|- class=\"$class\"\n";
- $r .= "!| {$v['name']}\n";
- $r .= "|| {$v['value']}\n";
+ $r .= "<tr class=\"$class\">\n";
+ $r .= "<th>{$v['name']}</th>\n";
+ $r .= "<td>{$v['value']}</td>\n</tr>";
}
}
- $r .= '|}';
+ $r .= "</table>\n</div>\n";
return $r;
}
@@ -274,7 +287,8 @@ class ImagePage extends Article {
}
protected function openShowImage() {
- global $wgOut, $wgUser, $wgImageLimits, $wgRequest, $wgLang, $wgContLang;
+ global $wgOut, $wgUser, $wgImageLimits, $wgRequest,
+ $wgLang, $wgContLang, $wgEnableUploads;
$this->loadFile();
@@ -315,7 +329,7 @@ class ImagePage extends Article {
$linkAttribs = array( 'href' => $full_url );
$longDesc = $this->displayImg->getLongDesc();
- wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this , &$wgOut ) ) ;
+ wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$wgOut ) );
if( $this->displayImg->allowInlineDisplay() ) {
# image
@@ -360,7 +374,8 @@ class ImagePage extends Article {
'<br />' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc;
}
- if( $this->displayImg->isMultipage() ) {
+ $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1;
+ if( $isMulti ) {
$wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
}
@@ -371,15 +386,21 @@ class ImagePage extends Article {
);
$wgOut->addHTML( '<div class="fullImageLink" id="file">' .
$thumbnail->toHtml( $options ) .
- $anchorclose . '</div>' );
+ $anchorclose . "</div>\n" );
}
- if( $this->displayImg->isMultipage() ) {
+ if( $isMulti ) {
$count = $this->displayImg->pageCount();
if( $page > 1 ) {
$label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
- $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page-1) );
+ $link = $sk->link(
+ $this->mTitle,
+ $label,
+ array(),
+ array( 'page' => $page - 1 ),
+ array( 'known', 'noclasses' )
+ );
$thumb1 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
array( 'page' => $page - 1 ) );
} else {
@@ -388,7 +409,13 @@ class ImagePage extends Article {
if( $page < $count ) {
$label = wfMsg( 'imgmultipagenext' );
- $link = $sk->makeKnownLinkObj( $this->mTitle, $label, 'page='. ($page+1) );
+ $link = $sk->link(
+ $this->mTitle,
+ $label,
+ array(),
+ array( 'page' => $page + 1 ),
+ array( 'known', 'noclasses' )
+ );
$thumb2 = $sk->makeThumbLinkObj( $this->mTitle, $this->displayImg, $link, $label, 'none',
array( 'page' => $page + 1 ) );
} else {
@@ -427,8 +454,8 @@ class ImagePage extends Article {
$icon= $this->displayImg->iconThumb();
$wgOut->addHTML( '<div class="fullImageLink" id="file">' .
- $icon->toHtml( array( 'desc-link' => true ) ) .
- '</div>' );
+ $icon->toHtml( array( 'file-link' => true ) ) .
+ "</div>\n" );
}
$showLink = true;
@@ -437,25 +464,26 @@ class ImagePage extends Article {
if($showLink) {
$filename = wfEscapeWikiText( $this->displayImg->getName() );
+ $medialink = "[[Media:$filename|$filename]]";
if( !$this->displayImg->isSafeFile() ) {
$warning = wfMsgNoTrans( 'mediawarning' );
$wgOut->addWikiText( <<<EOT
<div class="fullMedia">
-<span class="dangerousLink">[[Media:$filename|$filename]]</span>$dirmark
-<span class="fileInfo"> $longDesc</span>
+<span class="dangerousLink">{$medialink}</span>$dirmark
+<span class="fileInfo">$longDesc</span>
</div>
-
<div class="mediaWarning">$warning</div>
EOT
);
} else {
$wgOut->addWikiText( <<<EOT
<div class="fullMedia">
-[[Media:$filename|$filename]]$dirmark <span class="fileInfo"> $longDesc</span>
+{$medialink}{$dirmark}
+<span class="fileInfo">$longDesc</span>
</div>
EOT
- );
+ );
}
}
@@ -464,12 +492,20 @@ EOT
}
} else {
# Image does not exist
-
- $title = SpecialPage::getTitleFor( 'Upload' );
- $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'),
- 'wpDestFile=' . urlencode( $this->displayImg->getName() ) );
+ if ( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) {
+ // Only show an upload link if the user can upload
+ $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
+ $nofile = array(
+ 'filepage-nofile-link',
+ $uploadTitle->getFullUrl( array( 'wpDestFile' => $this->img->getName() ) )
+ );
+ }
+ else
+ {
+ $nofile = 'filepage-nofile';
+ }
$wgOut->setRobotPolicy( 'noindex,nofollow' );
- $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) );
+ $wgOut->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
}
}
@@ -477,26 +513,24 @@ EOT
* Show a notice that the file is from a shared repository
*/
protected function printSharedImageText() {
- global $wgOut, $wgUser;
+ global $wgOut;
$this->loadFile();
$descUrl = $this->img->getDescriptionUrl();
$descText = $this->img->getDescriptionText();
+
+ $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n";
+ $repo = $this->img->getRepo()->getDisplayName();
+
$msg = '';
- if( $descUrl ) {
- $sk = $wgUser->getSkin();
- $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) );
- $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki';
- $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link );
- if( $msg == '-' ) {
- $msg = '';
- }
+ if( $descUrl && $descText && wfMsgNoTrans( 'sharedupload-desc-here' ) !== '-' ) {
+ $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) );
+ } elseif ( $descUrl && wfMsgNoTrans( 'sharedupload-desc-there' ) !== '-' ) {
+ $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) );
+ } else {
+ $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ );
}
- $s = "<div class='sharedUploadNotice'>";
- $s .= wfMsgWikiHtml( 'sharedupload', $this->img->getRepo()->getDisplayName(), $msg );
- $s .= "</div>";
- $wgOut->addHTML( $s );
if( $descText ) {
$this->mExtraDescription = $descText;
@@ -506,7 +540,10 @@ EOT
public function getUploadUrl() {
$this->loadFile();
$uploadTitle = SpecialPage::getTitleFor( 'Upload' );
- return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) . '&wpForReUpload=1' );
+ return $uploadTitle->getFullUrl( array(
+ 'wpDestFile' => $this->img->getName(),
+ 'wpForReUpload' => 1
+ ) );
}
/**
@@ -514,7 +551,9 @@ EOT
* external editing (and instructions link) etc.
*/
protected function uploadLinksBox() {
- global $wgUser, $wgOut;
+ global $wgUser, $wgOut, $wgEnableUploads, $wgUseExternalEditor;
+
+ if( !$wgEnableUploads ) { return; }
$this->loadFile();
if( !$this->img->isLocal() )
@@ -522,19 +561,31 @@ EOT
$sk = $wgUser->getSkin();
- $wgOut->addHTML( '<br /><ul>' );
+ $wgOut->addHTML( "<br /><ul>\n" );
# "Upload a new version of this file" link
- if( UploadForm::userCanReUpload($wgUser,$this->img->name) ) {
+ if( UploadBase::userCanReUpload($wgUser,$this->img->name) ) {
$ulink = $sk->makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
- $wgOut->addHTML( "<li><div class='plainlinks'>{$ulink}</div></li>" );
+ $wgOut->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" );
}
# External editing link
- $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' );
- $wgOut->addHTML( '<li>' . $elink . ' <small>' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '</small></li>' );
+ if ( $wgUseExternalEditor ) {
+ $elink = $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'edit-externally' ),
+ array(),
+ array(
+ 'action' => 'edit',
+ 'externaledit' => 'true',
+ 'mode' => 'file'
+ ),
+ array( 'known', 'noclasses' )
+ );
+ $wgOut->addHTML( '<li id="mw-imagepage-edit-external">' . $elink . ' <small>' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . "</small></li>\n" );
+ }
- $wgOut->addHTML( '</ul>' );
+ $wgOut->addHTML( "</ul>\n" );
}
protected function closeShowImage() {} # For overloading
@@ -544,7 +595,7 @@ EOT
* we follow it with an upload history of the image and its usage.
*/
protected function imageHistory() {
- global $wgOut, $wgUseExternalEditor;
+ global $wgOut;
$this->loadFile();
$pager = new ImageHistoryPseudoPager( $this );
@@ -554,7 +605,7 @@ EOT
# Exist check because we don't want to show this on pages where an image
# doesn't exist along with the noimage message, that would suck. -ævar
- if( $wgUseExternalEditor && $this->img->exists() ) {
+ if( $this->img->exists() ) {
$this->uploadLinksBox();
}
}
@@ -599,17 +650,23 @@ EOT
$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, "" );
+ $link = $sk->link(
+ Title::makeTitle( $s->page_namespace, $s->page_title ),
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
$wgOut->addHTML( "<li>{$link}</li>\n" );
}
}
- $wgOut->addHTML( "</ul></div>\n" );
+ $wgOut->addHTML( "</ul>\n" );
$res->free();
// Add a links to [[Special:Whatlinkshere]]
if( $count > $limit )
$wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() );
+ $wgOut->addHTML( "</div>\n" );
}
protected function imageRedirects() {
@@ -626,7 +683,13 @@ EOT
$sk = $wgUser->getSkin();
foreach ( $redirects as $title ) {
- $link = $sk->makeKnownLinkObj( $title, "", "redirect=no" );
+ $link = $sk->link(
+ $title,
+ null,
+ array(),
+ array( 'redirect' => 'no' ),
+ array( 'known', 'noclasses' )
+ );
$wgOut->addHTML( "<li>{$link}</li>\n" );
}
$wgOut->addHTML( "</ul></div>\n" );
@@ -650,9 +713,15 @@ EOT
$sk = $wgUser->getSkin();
foreach ( $dupes as $file ) {
$fromSrc = '';
- if( $file->isLocal() )
- $link = $sk->makeKnownLinkObj( $file->getTitle(), "" );
- else {
+ if( $file->isLocal() ) {
+ $link = $sk->link(
+ $file->getTitle(),
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
+ } else {
$link = $sk->makeExternalLink( $file->getDescriptionUrl(),
$file->getTitle()->getPrefixedText() );
$fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() );
@@ -666,6 +735,13 @@ EOT
* Delete the file, or an earlier version of it
*/
public function delete() {
+ global $wgUploadMaintenance;
+ if( $wgUploadMaintenance && $this->mTitle && $this->mTitle->getNamespace() == NS_FILE ) {
+ global $wgOut;
+ $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", array( 'filedelete-maintenance' ) );
+ return;
+ }
+
$this->loadFile();
if( !$this->img->exists() || !$this->img->isLocal() || $this->img->getRedirected() ) {
// Standard article deletion
@@ -697,7 +773,10 @@ EOT
$this->img->upgradeRow();
$this->img->purgeCache();
} else {
- wfDebug( "ImagePage::doPurge no image\n" );
+ wfDebug( "ImagePage::doPurge no image for " . $this->img->getName() . "; limiting purge to cache only\n" );
+ // even if the file supposedly doesn't exist, force any cached information
+ // to be updated (in case the cached information is wrong)
+ $this->img->purgeCache();
}
parent::doPurge();
}
@@ -723,15 +802,16 @@ EOT
*/
class ImageHistoryList {
- protected $imagePage, $img, $skin, $title, $repo;
+ protected $imagePage, $img, $skin, $title, $repo, $showThumb;
public function __construct( $imagePage ) {
- global $wgUser;
+ global $wgUser, $wgShowArchiveThumbnails;
$this->skin = $wgUser->getSkin();
$this->current = $imagePage->getFile();
$this->img = $imagePage->getDisplayedFile();
$this->title = $imagePage->getTitle();
$this->imagePage = $imagePage;
+ $this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender();
}
public function getImagePage() {
@@ -748,14 +828,15 @@ class ImageHistoryList {
public function beginImageHistoryList( $navLinks = '' ) {
global $wgOut, $wgUser;
- return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) )
+ return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) . "\n"
+ . "<div id=\"mw-imagepage-section-filehistory\">\n"
. $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
- . $navLinks
- . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n"
+ . $navLinks . "\n"
+ . Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n"
. '<tr><td></td>'
- . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '<td></td>' : '' )
+ . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deletedhistory') ) ? '<td></td>' : '' )
. '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
- . '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>'
+ . ( $this->showThumb ? '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>' : '' )
. '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
. '<th>' . wfMsgHtml( 'filehist-user' ) . '</th>'
. '<th>' . wfMsgHtml( 'filehist-comment' ) . '</th>'
@@ -763,7 +844,7 @@ class ImageHistoryList {
}
public function endImageHistoryList( $navLinks = '' ) {
- return "</table>\n$navLinks\n";
+ return "</table>\n$navLinks\n</div>\n";
}
public function imageHistoryLine( $iscur, $file ) {
@@ -773,49 +854,45 @@ class ImageHistoryList {
$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') || $wgUser->isAllowed('deleterevision') ) ) {
+ if( $local && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deletedhistory') ) ) {
$row .= '<td>';
# Link to remove from history
if( $wgUser->isAllowed( 'delete' ) ) {
- $q = array();
- $q[] = 'action=delete';
+ $q = array( 'action' => 'delete' );
if( !$iscur )
- $q[] = 'oldimage=' . urlencode( $img );
- $row .= $this->skin->makeKnownLinkObj(
+ $q['oldimage'] = $img;
+ $row .= $this->skin->link(
$this->title,
wfMsgHtml( $iscur ? 'filehist-deleteall' : 'filehist-deleteone' ),
- implode( '&', $q )
+ array(), $q, array( 'known' )
);
}
- # Link to hide content
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ # Link to hide content. Don't show useless link to people who cannot hide revisions.
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $canHide || ($wgUser->isAllowed('deletedhistory') && $file->getVisibility()) ) {
if( $wgUser->isAllowed('delete') ) {
- $row .= '<br/>';
+ $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' );
+ $del = $this->skin->revDeleteLinkDisabled( $canHide );
} 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>";
+ list( $ts, $name ) = explode( '!', $img, 2 );
+ $query = array(
+ 'type' => 'oldimage',
+ 'target' => $wgTitle->getPrefixedText(),
+ 'ids' => $ts,
+ );
+ $del = $this->skin->revDeleteLink( $query,
+ $file->isDeleted(File::DELETED_RESTRICTED), $canHide );
}
- $row .= "<tt style='white-space: nowrap;'><small>$del</small></tt>";
+ $row .= $del;
}
$row .= '</td>';
}
@@ -828,13 +905,17 @@ class ImageHistoryList {
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,
+ $row .= $this->skin->link(
+ $this->title,
wfMsgHtml( 'filehist-revert' ),
- implode( '&', $q ) );
+ array(),
+ array(
+ 'action' => 'revert',
+ 'oldimage' => $img,
+ 'wpEditToken' => $wgUser->editToken( $img )
+ ),
+ array( 'known', 'noclasses' )
+ );
}
}
$row .= '</td>';
@@ -847,43 +928,40 @@ class ImageHistoryList {
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) ) {
+ } elseif( $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() );
+ $url = $this->skin->link(
+ $revdel,
+ $wgLang->timeAndDate( $timestamp, true ),
+ array(),
+ array(
+ 'target' => $wgTitle->getPrefixedText(),
+ 'file' => $img,
+ 'token' => $wgUser->editToken( $img )
+ ),
+ array( 'known', 'noclasses' )
+ );
$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>";
// Thumbnail
- if( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) {
- $params = array(
- 'width' => '120',
- 'height' => '120',
- );
- $thumbnail = $file->transform( $params );
- $options = array(
- 'alt' => wfMsg( 'filehist-thumbtext', $wgLang->timeAndDate( $timestamp, true ) ),
- 'file-link' => true,
- );
- $row .= '</td><td>' . ( $thumbnail ? $thumbnail->toHtml( $options ) :
- wfMsgHtml( 'filehist-nothumb' ) );
- } else {
- $row .= '</td><td>' . wfMsgHtml( 'filehist-nothumb' );
+ if ( $this->showThumb ) {
+ $row .= '<td>' . $this->getThumbForLine( $file ) . '</td>';
}
- $row .= "</td><td>";
-
- // Image dimensions
- $row .= htmlspecialchars( $dims );
- // File size
- $row .= " <span style='white-space: nowrap;'>(" . $this->skin->formatSize( $size ) . ')</span>';
+ // Image dimensions + size
+ $row .= '<td>';
+ $row .= htmlspecialchars( $file->getDimensionsString() );
+ $row .= " <span style='white-space: nowrap;'>(" . $this->skin->formatSize( $file->getSize() ) . ')</span>';
+ $row .= '</td>';
// Uploading user
- $row .= '</td><td>';
+ $row .= '<td>';
if( $local ) {
// Hide deleted usernames
if( $file->isDeleted(File::DELETED_USER) ) {
@@ -910,6 +988,33 @@ class ImageHistoryList {
return "<tr{$classAttr}>{$row}</tr>\n";
}
+
+ protected function getThumbForLine( $file ) {
+ global $wgLang;
+
+ if( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) {
+ $params = array(
+ 'width' => '120',
+ 'height' => '120',
+ );
+ $timestamp = wfTimestamp(TS_MW, $file->getTimestamp());
+
+ $thumbnail = $file->transform( $params );
+ $options = array(
+ 'alt' => wfMsg( 'filehist-thumbtext',
+ $wgLang->timeAndDate( $timestamp, true ),
+ $wgLang->date( $timestamp, true ),
+ $wgLang->time( $timestamp, true ) ),
+ 'file-link' => true,
+ );
+
+ if ( !$thumbnail ) return wfMsgHtml( 'filehist-nothumb' );
+
+ return $thumbnail->toHtml( $options );
+ } else {
+ return wfMsgHtml( 'filehist-nothumb' );
+ }
+ }
}
class ImageHistoryPseudoPager extends ReverseChronologicalPager {
@@ -918,7 +1023,7 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager {
$this->mImagePage = $imagePage;
$this->mTitle = clone( $imagePage->getTitle() );
$this->mTitle->setFragment( '#filehistory' );
- $this->mImg = NULL;
+ $this->mImg = null;
$this->mHist = array();
$this->mRange = array( 0, 0 ); // display range
}
diff --git a/includes/ImageQueryPage.php b/includes/ImageQueryPage.php
index 3ab0b858..180201a2 100644
--- a/includes/ImageQueryPage.php
+++ b/includes/ImageQueryPage.php
@@ -13,12 +13,12 @@ class ImageQueryPage extends QueryPage {
* Format and output report results using the given information plus
* OutputPage
*
- * @param OutputPage $out OutputPage to print to
- * @param Skin $skin User skin to use
- * @param Database $dbr Database (read) connection to use
- * @param int $res Result pointer
- * @param int $num Number of available result rows
- * @param int $offset Paging offset
+ * @param $out OutputPage to print to
+ * @param $skin Skin: user skin to use
+ * @param $dbr Database (read) connection to use
+ * @param $res Integer: result pointer
+ * @param $num Integer: number of available result rows
+ * @param $offset Integer: paging offset
*/
protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
if( $num > 0 ) {
@@ -41,7 +41,7 @@ class ImageQueryPage extends QueryPage {
/**
* Prepare an image object given a result row
*
- * @param object $row Result row
+ * @param $row Object: result row
* @return Image
*/
private function prepareImage( $row ) {
@@ -55,8 +55,8 @@ class ImageQueryPage extends QueryPage {
/**
* Get additional HTML to be shown in a results' cell
*
- * @param object $row Result row
- * @return string
+ * @param $row Object: result row
+ * @return String
*/
protected function getCellHtml( $row ) {
return '';
diff --git a/includes/Import.php b/includes/Import.php
index 973866df..45908a66 100644
--- a/includes/Import.php
+++ b/includes/Import.php
@@ -265,7 +265,7 @@ class WikiRevision {
$this->timestamp . "\n" );
return false;
}
- $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' );
+ $log_id = $dbw->nextSequenceValue( 'logging_log_id_seq' );
$data = array(
'log_id' => $log_id,
'log_type' => $this->type,
@@ -304,7 +304,7 @@ class WikiRevision {
$resultDetails = array( 'internal' => $status->getWikiText() );
*/
- // @fixme upload() uses $wgUser, which is wrong here
+ // @todo 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
@@ -352,7 +352,7 @@ class WikiRevision {
return false;
}
- // @fixme!
+ // @todo Fixme!
$src = $this->getSrc();
$data = Http::get( $src );
if( !$data ) {
@@ -400,21 +400,21 @@ class WikiImporter {
}
function handleXmlNamespace ( $parser, $data, $prefix=false, $uri=false ) {
- if( preg_match( '/www.mediawiki.org/',$prefix ) ) {
- $prefix = str_replace( '/','\/',$prefix );
+ if( preg_match( '/www.mediawiki.org/',$prefix ) ) {
+ $prefix = str_replace( '/','\/',$prefix );
$this->mXmlNamespace='/^'.$prefix.':/';
}
}
function stripXmlNamespace($name) {
if( $this->mXmlNamespace ) {
- return(preg_replace($this->mXmlNamespace,'',$name,1));
+ return(preg_replace($this->mXmlNamespace,'',$name,1));
}
else {
- return($name);
- }
+ return($name);
+ }
}
-
+
# --------------
function doImport() {
@@ -554,7 +554,7 @@ class WikiImporter {
/**
* Default per-revision callback, performs the import.
- * @param $revision WikiRevision
+ * @param $rev WikiRevision
* @private
*/
function importLogItem( $rev ) {
@@ -621,7 +621,7 @@ class WikiImporter {
}
function in_start( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_start $name" );
if( $name != "mediawiki" ) {
return $this->throwXMLerror( "Expected <mediawiki>, got <$name>" );
@@ -630,7 +630,7 @@ class WikiImporter {
}
function in_mediawiki( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_mediawiki $name" );
if( $name == 'siteinfo' ) {
xml_set_element_handler( $parser, "in_siteinfo", "out_siteinfo" );
@@ -650,7 +650,7 @@ class WikiImporter {
}
}
function out_mediawiki( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_mediawiki $name" );
if( $name != "mediawiki" ) {
return $this->throwXMLerror( "Expected </mediawiki>, got </$name>" );
@@ -661,7 +661,7 @@ class WikiImporter {
function in_siteinfo( $parser, $name, $attribs ) {
// no-ops for now
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_siteinfo $name" );
switch( $name ) {
case "sitename":
@@ -677,7 +677,7 @@ class WikiImporter {
}
function out_siteinfo( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
if( $name == "siteinfo" ) {
xml_set_element_handler( $parser, "in_mediawiki", "out_mediawiki" );
}
@@ -685,11 +685,12 @@ class WikiImporter {
function in_page( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_page $name" );
switch( $name ) {
case "id":
case "title":
+ case "redirect":
case "restrictions":
$this->appendfield = $name;
$this->appenddata = "";
@@ -726,7 +727,7 @@ class WikiImporter {
}
function out_page( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_page $name" );
$this->pop();
if( $name != "page" ) {
@@ -746,7 +747,7 @@ class WikiImporter {
}
function in_nothing( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_nothing $name" );
return $this->throwXMLerror( "No child elements allowed here; got <$name>" );
}
@@ -757,7 +758,7 @@ class WikiImporter {
}
function out_append( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_append $name" );
if( $name != $this->appendfield ) {
return $this->throwXMLerror( "Expected </{$this->appendfield}>, got </$name>" );
@@ -853,7 +854,7 @@ class WikiImporter {
}
function in_revision( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_revision $name" );
switch( $name ) {
case "id":
@@ -875,7 +876,7 @@ class WikiImporter {
}
function out_revision( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_revision $name" );
$this->pop();
if( $name != "revision" ) {
@@ -891,9 +892,9 @@ class WikiImporter {
}
}
}
-
+
function in_logitem( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_logitem $name" );
switch( $name ) {
case "id":
@@ -917,7 +918,7 @@ class WikiImporter {
}
function out_logitem( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_logitem $name" );
$this->pop();
if( $name != "logitem" ) {
@@ -935,7 +936,7 @@ class WikiImporter {
}
function in_upload( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_upload $name" );
switch( $name ) {
case "timestamp":
@@ -958,7 +959,7 @@ class WikiImporter {
}
function out_upload( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_revision $name" );
$this->pop();
if( $name != "upload" ) {
@@ -976,7 +977,7 @@ class WikiImporter {
}
function in_contributor( $parser, $name, $attribs ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "in_contributor $name" );
switch( $name ) {
case "username":
@@ -992,7 +993,7 @@ class WikiImporter {
}
function out_contributor( $parser, $name ) {
- $name = $this->stripXmlNamespace($name);
+ $name = $this->stripXmlNamespace($name);
$this->debug( "out_contributor $name" );
$this->pop();
if( $name != "contributor" ) {
@@ -1084,9 +1085,9 @@ class ImportStreamSource {
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
+ 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
}
}
diff --git a/includes/Interwiki.php b/includes/Interwiki.php
index 3522fadb..3c71f6ee 100644
--- a/includes/Interwiki.php
+++ b/includes/Interwiki.php
@@ -17,8 +17,7 @@ class Interwiki {
protected $mPrefix, $mURL, $mLocal, $mTrans;
- function __construct( $prefix = null, $url = '', $local = 0, $trans = 0 )
- {
+ public function __construct( $prefix = null, $url = '', $local = 0, $trans = 0 ) {
$this->mPrefix = $prefix;
$this->mURL = $url;
$this->mLocal = $local;
@@ -27,20 +26,20 @@ class Interwiki {
/**
* Check whether an interwiki prefix exists
- *
- * @return bool Whether it exists
- * @param $prefix string Interwiki prefix to use
+ *
+ * @param $prefix String: interwiki prefix to use
+ * @return Boolean: whether it exists
*/
- static public function isValidInterwiki( $prefix ){
+ static public function isValidInterwiki( $prefix ) {
$result = self::fetch( $prefix );
return (bool)$result;
}
/**
* Fetch an Interwiki object
- *
+ *
+ * @param $prefix String: interwiki prefix to use
* @return Interwiki Object, or null if not valid
- * @param $prefix string Interwiki prefix to use
*/
static public function fetch( $prefix ) {
global $wgContLang;
@@ -48,85 +47,85 @@ class Interwiki {
return null;
}
$prefix = $wgContLang->lc( $prefix );
- if( isset( self::$smCache[$prefix] ) ){
+ if( isset( self::$smCache[$prefix] ) ) {
return self::$smCache[$prefix];
}
global $wgInterwikiCache;
- if ($wgInterwikiCache) {
+ if( $wgInterwikiCache ) {
$iw = Interwiki::getInterwikiCached( $prefix );
} else {
$iw = Interwiki::load( $prefix );
- if( !$iw ){
+ if( !$iw ) {
$iw = false;
}
}
- if( self::CACHE_LIMIT && count( self::$smCache ) >= self::CACHE_LIMIT ){
+ if( self::CACHE_LIMIT && count( self::$smCache ) >= self::CACHE_LIMIT ) {
reset( self::$smCache );
unset( self::$smCache[ key( self::$smCache ) ] );
}
self::$smCache[$prefix] = $iw;
return $iw;
}
-
+
/**
* Fetch interwiki prefix data from local cache in constant database.
*
* @note More logic is explained in DefaultSettings.
*
- * @param $prefix \type{\string} Interwiki prefix
- * @return \type{\Interwiki} An interwiki object
+ * @param $prefix String: interwiki prefix
+ * @return Interwiki object
*/
protected static function getInterwikiCached( $prefix ) {
$value = self::getInterwikiCacheEntry( $prefix );
-
+
$s = new Interwiki( $prefix );
if ( $value != '' ) {
// Split values
list( $local, $url ) = explode( ' ', $value, 2 );
$s->mURL = $url;
$s->mLocal = (int)$local;
- }else{
+ } else {
$s = false;
}
return $s;
}
-
+
/**
* Get entry from interwiki cache
*
* @note More logic is explained in DefaultSettings.
*
- * @param $prefix \type{\string} Database key
- * @return \type{\string) The entry
+ * @param $prefix String: database key
+ * @return String: the entry
*/
- protected static function getInterwikiCacheEntry( $prefix ){
+ protected static function getInterwikiCacheEntry( $prefix ) {
global $wgInterwikiCache, $wgInterwikiScopes, $wgInterwikiFallbackSite;
static $db, $site;
wfDebug( __METHOD__ . "( $prefix )\n" );
- if( !$db ){
- $db = dba_open( $wgInterwikiCache, 'r', 'cdb' );
+ if( !$db ) {
+ $db = CdbReader::open( $wgInterwikiCache );
}
/* Resolve site name */
if( $wgInterwikiScopes>=3 && !$site ) {
- $site = dba_fetch( '__sites:' . wfWikiID(), $db );
- if ( $site == "" ){
+ $site = $db->get( '__sites:' . wfWikiID() );
+ if ( $site == '' ) {
$site = $wgInterwikiFallbackSite;
}
}
-
- $value = dba_fetch( wfMemcKey( $prefix ), $db );
+
+ $value = $db->get( wfMemcKey( $prefix ) );
// Site level
if ( $value == '' && $wgInterwikiScopes >= 3 ) {
- $value = dba_fetch( "_{$site}:{$prefix}", $db );
+ $value = $db->get( "_{$site}:{$prefix}" );
}
// Global Level
if ( $value == '' && $wgInterwikiScopes >= 2 ) {
- $value = dba_fetch( "__global:{$prefix}", $db );
+ $value = $db->get( "__global:{$prefix}" );
}
if ( $value == 'undef' )
$value = '';
-
+
return $value;
}
@@ -134,24 +133,22 @@ class Interwiki {
* Load the interwiki, trying first memcached then the DB
*
* @param $prefix The interwiki prefix
- * @return bool The prefix is valid
- * @static
- *
+ * @return Boolean: the prefix is valid
*/
protected static function load( $prefix ) {
global $wgMemc, $wgInterwikiExpiry;
$key = wfMemcKey( 'interwiki', $prefix );
$mc = $wgMemc->get( $key );
$iw = false;
- if( $mc && is_array( $mc ) ){ // is_array is hack for old keys
+ if( $mc && is_array( $mc ) ) { // is_array is hack for old keys
$iw = Interwiki::loadFromArray( $mc );
- if( $iw ){
+ if( $iw ) {
return $iw;
}
}
-
+
$db = wfGetDB( DB_SLAVE );
-
+
$row = $db->fetchRow( $db->select( 'interwiki', '*', array( 'iw_prefix' => $prefix ),
__METHOD__ ) );
$iw = Interwiki::loadFromArray( $row );
@@ -160,19 +157,18 @@ class Interwiki {
$wgMemc->add( $key, $mc, $wgInterwikiExpiry );
return $iw;
}
-
+
return false;
}
/**
* Fill in member variables from an array (e.g. memcached result, Database::fetchRow, etc)
*
- * @return bool Whether everything was there
- * @param $res ResultWrapper Row from the interwiki table
- * @static
+ * @param $mc Associative array: row from the interwiki table
+ * @return Boolean: whether everything was there
*/
protected static function loadFromArray( $mc ) {
- if( isset( $mc['iw_url'] ) && isset( $mc['iw_local'] ) && isset( $mc['iw_trans'] ) ){
+ if( isset( $mc['iw_url'] ) && isset( $mc['iw_local'] ) && isset( $mc['iw_trans'] ) ) {
$iw = new Interwiki();
$iw->mURL = $mc['iw_url'];
$iw->mLocal = $mc['iw_local'];
@@ -181,27 +177,60 @@ class Interwiki {
}
return false;
}
-
- /**
+
+ /**
* Get the URL for a particular title (or with $1 if no title given)
*
- * @param $title string What text to put for the article name
- * @return string The URL
+ * @param $title String: what text to put for the article name
+ * @return String: the URL
*/
- function getURL( $title = null ){
+ public function getURL( $title = null ) {
$url = $this->mURL;
- if( $title != null ){
+ if( $title != null ) {
$url = str_replace( "$1", $title, $url );
}
return $url;
}
-
- function isLocal(){
+
+ /**
+ * Is this a local link from a sister project, or is
+ * it something outside, like Google
+ *
+ * @return Boolean
+ */
+ public function isLocal() {
return $this->mLocal;
}
-
- function isTranscludable(){
+
+ /**
+ * Can pages from this wiki be transcluded?
+ * Still requires $wgEnableScaryTransclusion
+ *
+ * @return Boolean
+ */
+ public function isTranscludable() {
return $this->mTrans;
}
+ /**
+ * Get the name for the interwiki site
+ *
+ * @return String
+ */
+ public function getName() {
+ $key = 'interwiki-name-' . $this->mPrefix;
+ $msg = wfMsgForContent( $key );
+ return wfEmptyMsg( $key, $msg ) ? '' : $msg;
+ }
+
+ /**
+ * Get a description for this interwiki
+ *
+ * @return String
+ */
+ public function getDescription() {
+ $key = 'interwiki-desc-' . $this->mPrefix;
+ $msg = wfMsgForContent( $key );
+ return wfEmptyMsg( $key, $msg ) ? '' : $msg;
+ }
}
diff --git a/includes/JSMin.php b/includes/JSMin.php
new file mode 100644
index 00000000..70db7022
--- /dev/null
+++ b/includes/JSMin.php
@@ -0,0 +1,290 @@
+<?php
+/**
+ * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+ *
+ * This is pretty much a direct port of jsmin.c to PHP with just a few
+ * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
+ * outputs to stdout, this library accepts a string as input and returns another
+ * string as output.
+ *
+ * PHP 5 or higher is required.
+ *
+ * Permission is hereby granted to use this version of the library under the
+ * same terms as jsmin.c, which has the following license:
+ *
+ * --
+ * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is furnished to do
+ * so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * The Software shall be used for Good, not Evil.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ * --
+ *
+ * @package JSMin
+ * @author Ryan Grove <ryan@wonko.com>
+ * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
+ * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
+ * @license http://opensource.org/licenses/mit-license.php MIT License
+ * @version 1.1.1 (2008-03-02)
+ * @link http://code.google.com/p/jsmin-php/
+ */
+
+class JSMin {
+ const ORD_LF = 10;
+ const ORD_SPACE = 32;
+
+ protected $a = '';
+ protected $b = '';
+ protected $input = '';
+ protected $inputIndex = 0;
+ protected $inputLength = 0;
+ protected $lookAhead = null;
+ protected $output = '';
+
+ // -- Public Static Methods --------------------------------------------------
+
+ public static function minify( $js ) {
+ $jsmin = new JSMin( $js );
+ return $jsmin->min();
+ }
+
+ // -- Public Instance Methods ------------------------------------------------
+
+ public function __construct( $input ) {
+ $this->input = str_replace( "\r\n", "\n", $input );
+ $this->inputLength = strlen( $this->input );
+ }
+
+ // -- Protected Instance Methods ---------------------------------------------
+
+ protected function action( $d ) {
+ switch( $d ) {
+ case 1:
+ $this->output .= $this->a;
+
+ case 2:
+ $this->a = $this->b;
+
+ if ( $this->a === "'" || $this->a === '"' ) {
+ for ( ; ; ) {
+ $this->output .= $this->a;
+ $this->a = $this->get();
+
+ if ( $this->a === $this->b ) {
+ break;
+ }
+
+ if ( ord( $this->a ) <= self::ORD_LF ) {
+ throw new JSMinException( 'Unterminated string literal.' );
+ }
+
+ if ( $this->a === '\\' ) {
+ $this->output .= $this->a;
+ $this->a = $this->get();
+ }
+ }
+ }
+
+ case 3:
+ $this->b = $this->next();
+
+ if ( $this->b === '/' && (
+ $this->a === '(' || $this->a === ',' || $this->a === '=' ||
+ $this->a === ':' || $this->a === '[' || $this->a === '!' ||
+ $this->a === '&' || $this->a === '|' || $this->a === '?' ) ) {
+
+ $this->output .= $this->a . $this->b;
+
+ for ( ; ; ) {
+ $this->a = $this->get();
+
+ if ( $this->a === '/' ) {
+ break;
+ } elseif ( $this->a === '\\' ) {
+ $this->output .= $this->a;
+ $this->a = $this->get();
+ } elseif ( ord( $this->a ) <= self::ORD_LF ) {
+ throw new JSMinException( 'Unterminated regular expression ' .
+ 'literal.' );
+ }
+
+ $this->output .= $this->a;
+ }
+
+ $this->b = $this->next();
+ }
+ }
+ }
+
+ protected function get() {
+ $c = $this->lookAhead;
+ $this->lookAhead = null;
+
+ if ( $c === null ) {
+ if ( $this->inputIndex < $this->inputLength ) {
+ $c = substr( $this->input, $this->inputIndex, 1 );
+ $this->inputIndex += 1;
+ } else {
+ $c = null;
+ }
+ }
+
+ if ( $c === "\r" ) {
+ return "\n";
+ }
+
+ if ( $c === null || $c === "\n" || ord( $c ) >= self::ORD_SPACE ) {
+ return $c;
+ }
+
+ return ' ';
+ }
+
+ protected function isAlphaNum( $c ) {
+ return ord( $c ) > 126 || $c === '\\' || preg_match( '/^[\w\$]$/', $c ) === 1;
+ }
+
+ protected function min() {
+ $this->a = "\n";
+ $this->action( 3 );
+
+ while ( $this->a !== null ) {
+ switch ( $this->a ) {
+ case ' ':
+ if ( $this->isAlphaNum( $this->b ) ) {
+ $this->action( 1 );
+ } else {
+ $this->action( 2 );
+ }
+ break;
+
+ case "\n":
+ switch ( $this->b ) {
+ case '{':
+ case '[':
+ case '(':
+ case '+':
+ case '-':
+ $this->action( 1 );
+ break;
+
+ case ' ':
+ $this->action( 3 );
+ break;
+
+ default:
+ if ( $this->isAlphaNum( $this->b ) ) {
+ $this->action( 1 );
+ }
+ else {
+ $this->action( 2 );
+ }
+ }
+ break;
+
+ default:
+ switch ( $this->b ) {
+ case ' ':
+ if ( $this->isAlphaNum( $this->a ) ) {
+ $this->action( 1 );
+ break;
+ }
+
+ $this->action( 3 );
+ break;
+
+ case "\n":
+ switch ( $this->a ) {
+ case '}':
+ case ']':
+ case ')':
+ case '+':
+ case '-':
+ case '"':
+ case "'":
+ $this->action( 1 );
+ break;
+
+ default:
+ if ( $this->isAlphaNum( $this->a ) ) {
+ $this->action( 1 );
+ }
+ else {
+ $this->action( 3 );
+ }
+ }
+ break;
+
+ default:
+ $this->action( 1 );
+ break;
+ }
+ }
+ }
+
+ return $this->output;
+ }
+
+ protected function next() {
+ $c = $this->get();
+
+ if ( $c === '/' ) {
+ switch( $this->peek() ) {
+ case '/':
+ for ( ; ; ) {
+ $c = $this->get();
+
+ if ( ord( $c ) <= self::ORD_LF ) {
+ return $c;
+ }
+ }
+
+ case '*':
+ $this->get();
+
+ for ( ; ; ) {
+ switch( $this->get() ) {
+ case '*':
+ if ( $this->peek() === '/' ) {
+ $this->get();
+ return ' ';
+ }
+ break;
+
+ case null:
+ throw new JSMinException( 'Unterminated comment.' );
+ }
+ }
+
+ default:
+ return $c;
+ }
+ }
+
+ return $c;
+ }
+
+ protected function peek() {
+ $this->lookAhead = $this->get();
+ return $this->lookAhead;
+ }
+}
+
+// -- Exceptions ---------------------------------------------------------------
+class JSMinException extends Exception {}
diff --git a/includes/JobQueue.php b/includes/JobQueue.php
index afa757d7..4ab5eac6 100644
--- a/includes/JobQueue.php
+++ b/includes/JobQueue.php
@@ -46,16 +46,20 @@ abstract class Job {
* actually find a job; it may be adversely affected by concurrent job
* runners.
*/
- static function pop_type($type) {
+ static function pop_type( $type ) {
wfProfilein( __METHOD__ );
$dbw = wfGetDB( DB_MASTER );
+ $row = $dbw->selectRow(
+ 'job',
+ '*',
+ array( 'job_cmd' => $type ),
+ __METHOD__,
+ array( 'LIMIT' => 1 )
+ );
- $row = $dbw->selectRow( 'job', '*', array( 'job_cmd' => $type ), __METHOD__,
- array( 'LIMIT' => 1 ));
-
- if ($row === false) {
+ if ( $row === false ) {
wfProfileOut( __METHOD__ );
return false;
}
@@ -64,7 +68,7 @@ abstract class Job {
$dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
$affected = $dbw->affectedRows();
- if ($affected == 0) {
+ if ( $affected == 0 ) {
wfProfileOut( __METHOD__ );
return false;
}
@@ -75,7 +79,7 @@ abstract class Job {
$job = Job::factory( $row->job_cmd, $title, Job::extractBlob( $row->job_params ), $row->job_id );
$dbw->delete( 'job', $job->insertFields(), __METHOD__ );
- $dbw->immediateCommit();
+ $dbw->commit();
wfProfileOut( __METHOD__ );
return $job;
@@ -84,10 +88,10 @@ abstract class Job {
/**
* Pop a job off the front of the queue
*
- * @param $offset Number of jobs to skip
+ * @param $offset Integer: Number of jobs to skip
* @return Job or false if there's no jobs
*/
- static function pop($offset=0) {
+ static function pop( $offset = 0 ) {
wfProfileIn( __METHOD__ );
$dbr = wfGetDB( DB_SLAVE );
@@ -100,17 +104,18 @@ abstract class Job {
*/
$row = $dbr->selectRow( 'job', '*', "job_id >= ${offset}", __METHOD__,
- array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ));
+ array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ) );
// Refetching without offset is needed as some of job IDs could have had delayed commits
// and have lower IDs than jobs already executed, blame concurrency :)
//
- if ( $row === false) {
- if ($offset!=0)
+ if ( $row === false ) {
+ if ( $offset != 0 ) {
$row = $dbr->selectRow( 'job', '*', '', __METHOD__,
- array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ));
+ array( 'ORDER BY' => 'job_id', 'LIMIT' => 1 ) );
+ }
- if ($row === false ) {
+ if ( $row === false ) {
wfProfileOut( __METHOD__ );
return false;
}
@@ -121,7 +126,7 @@ abstract class Job {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
$affected = $dbw->affectedRows();
- $dbw->immediateCommit();
+ $dbw->commit();
if ( !$affected ) {
// Failed, someone else beat us to it
@@ -135,7 +140,7 @@ abstract class Job {
}
// Get the random row
$row = $dbw->selectRow( 'job', '*',
- 'job_id >= ' . mt_rand( $row->minjob, $row->maxjob ), __METHOD__ );
+ 'job_id >= ' . mt_rand( $row->minjob, $row->maxjob ), __METHOD__ );
if ( $row === false ) {
// Random job gone before we got the chance to select it
// Give up
@@ -145,7 +150,7 @@ abstract class Job {
// Delete the random row
$dbw->delete( 'job', array( 'job_id' => $row->job_id ), __METHOD__ );
$affected = $dbw->affectedRows();
- $dbw->immediateCommit();
+ $dbw->commit();
if ( !$affected ) {
// Random job gone before we exclusively deleted it
@@ -267,12 +272,13 @@ abstract class Job {
return;
}
}
- $fields['job_id'] = $dbw->nextSequenceValue( 'job_job_id_seq' );
$dbw->insert( 'job', $fields, __METHOD__ );
}
protected function insertFields() {
+ $dbw = wfGetDB( DB_MASTER );
return array(
+ 'job_id' => $dbw->nextSequenceValue( 'job_job_id_seq' ),
'job_cmd' => $this->command,
'job_namespace' => $this->title->getNamespace(),
'job_title' => $this->title->getDBkey(),
diff --git a/includes/Licenses.php b/includes/Licenses.php
index 6398c887..12a1f938 100644
--- a/includes/Licenses.php
+++ b/includes/Licenses.php
@@ -9,46 +9,39 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
-class Licenses {
- /**#@+
- * @private
- */
+class Licenses extends HTMLFormField {
/**
* @var string
*/
- var $msg;
+ protected $msg;
/**
* @var array
*/
- var $licenses = array();
+ protected $licenses = array();
/**
* @var string
*/
- var $html;
+ protected $html;
/**#@-*/
/**
* Constructor
- *
- * @param $str String: the string to build the licenses member from, will use
- * wfMsgForContent( 'licenses' ) if null (default: null)
*/
- function __construct( $str = null ) {
- // PHP sucks, this should be possible in the constructor
- $this->msg = is_null( $str ) ? wfMsgForContent( 'licenses' ) : $str;
- $this->html = '';
+ public function __construct( $params ) {
+ parent::__construct( $params );
+
+ $this->msg = empty( $params['licenses'] ) ? wfMsgForContent( 'licenses' ) : $params['licenses'];
+ $this->selected = null;
$this->makeLicenses();
- $tmp = $this->getLicenses();
- $this->makeHtml( $tmp );
}
/**#@+
* @private
*/
- function makeLicenses() {
+ protected function makeLicenses() {
$levels = array();
$lines = explode( "\n", $this->msg );
@@ -75,18 +68,14 @@ class Licenses {
}
}
- function trimStars( $str ) {
+ protected function trimStars( $str ) {
$i = $count = 0;
- wfSuppressWarnings();
- while ($str[$i++] == '*')
- ++$count;
- wfRestoreWarnings();
-
- return array( $count, ltrim( $str, '* ' ) );
+ $numStars = strspn( $str, '*' );
+ return array( $numStars, ltrim( substr( $str, $numStars ), ' ' ) );
}
- function stackItem( &$list, $path, $item ) {
+ protected function stackItem( &$list, $path, $item ) {
$position =& $list;
if ( $path )
foreach( $path as $key )
@@ -94,13 +83,12 @@ class Licenses {
$position[] = $item;
}
- function makeHtml( &$tagset, $depth = 0 ) {
+ protected function makeHtml( $tagset, $depth = 0 ) {
foreach ( $tagset as $key => $val )
if ( is_array( $val ) ) {
$this->html .= $this->outputOption(
- $this->msg( $key ),
+ $this->msg( $key ), '',
array(
- 'value' => '',
'disabled' => 'disabled',
'style' => 'color: GrayText', // for MSIE
),
@@ -109,22 +97,22 @@ class Licenses {
$this->makeHtml( $val, $depth + 1 );
} else {
$this->html .= $this->outputOption(
- $this->msg( $val->text ),
- array(
- 'value' => $val->template,
- 'title' => '{{' . $val->template . '}}'
- ),
+ $this->msg( $val->text ), $val->template,
+ array( 'title' => '{{' . $val->template . '}}' ),
$depth
);
}
}
- function outputOption( $val, $attribs = null, $depth ) {
- $val = str_repeat( /* &nbsp */ "\xc2\xa0", $depth * 2 ) . $val;
+ protected function outputOption( $text, $value, $attribs = null, $depth = 0 ) {
+ $attribs['value'] = $value;
+ if ( $value === $this->selected )
+ $attribs['selected'] = 'selected';
+ $val = str_repeat( /* &nbsp */ "\xc2\xa0", $depth * 2 ) . $text;
return str_repeat( "\t", $depth ) . Xml::element( 'option', $attribs, $val ) . "\n";
}
- function msg( $str ) {
+ protected function msg( $str ) {
$out = wfMsg( $str );
return wfEmptyMsg( $str, $out ) ? $str : $out;
}
@@ -136,14 +124,29 @@ class Licenses {
*
* @return array
*/
- function getLicenses() { return $this->licenses; }
+ public function getLicenses() { return $this->licenses; }
/**
* Accessor for $this->html
*
* @return string
*/
- function getHtml() { return $this->html; }
+ public function getInputHTML( $value ) {
+ $this->selected = $value;
+
+ $this->html = $this->outputOption( wfMsg( 'nolicense' ), '',
+ (bool)$this->selected ? null : array( 'selected' => 'selected' ) );
+ $this->makeHtml( $this->getLicenses() );
+
+ $attribs = array(
+ 'name' => $this->mName,
+ 'id' => $this->mID
+ );
+ if ( !empty( $this->mParams['disabled'] ) )
+ $attibs['disabled'] = 'disabled';
+
+ return Html::rawElement( 'select', $attribs, $this->html );
+ }
}
/**
diff --git a/includes/LinkCache.php b/includes/LinkCache.php
index 4f74cdd7..8d035763 100644
--- a/includes/LinkCache.php
+++ b/includes/LinkCache.php
@@ -33,7 +33,7 @@ class LinkCache {
/**
* General accessor to get/set whether SELECT FOR UPDATE should be used
*/
- public function forUpdate( $update = NULL ) {
+ public function forUpdate( $update = null ) {
return wfSetVar( $this->mForUpdate, $update );
}
@@ -57,7 +57,7 @@ class LinkCache {
if ( array_key_exists( $dbkey, $this->mGoodLinkFields ) ) {
return $this->mGoodLinkFields[$dbkey][$field];
} else {
- return NULL;
+ return null;
}
}
@@ -72,10 +72,12 @@ class LinkCache {
* @param int $len
* @param int $redir
*/
- public function addGoodLinkObj( $id, $title, $len = -1, $redir = NULL ) {
+ 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->mGoodLinks[$dbkey] = intval( $id );
+ $this->mGoodLinkFields[$dbkey] = array(
+ 'length' => intval( $len ),
+ 'redirect' => intval( $redir ) );
}
public function addBadLinkObj( $title ) {
@@ -112,7 +114,7 @@ class LinkCache {
* @param $redir bool, is redirect?
* @return integer
*/
- public function addLink( $title, $len = -1, $redir = NULL ) {
+ public function addLink( $title, $len = -1, $redir = null ) {
$nt = Title::newFromDBkey( $title );
if( $nt ) {
return $this->addLinkObj( $nt, $len, $redir );
@@ -128,7 +130,7 @@ class LinkCache {
* @param $redir bool, is redirect?
* @return integer
*/
- public function addLinkObj( &$nt, $len = -1, $redirect = NULL ) {
+ public function addLinkObj( &$nt, $len = -1, $redirect = null ) {
global $wgAntiLockFlags, $wgProfiler;
wfProfileIn( __METHOD__ );
@@ -167,9 +169,9 @@ class LinkCache {
__METHOD__, $options );
# Set fields...
if ( $s !== false ) {
- $id = $s->page_id;
- $len = $s->page_len;
- $redirect = $s->page_is_redirect;
+ $id = intval( $s->page_id );
+ $len = intval( $s->page_len );
+ $redirect = intval( $s->page_is_redirect );
} else {
$len = -1;
$redirect = 0;
diff --git a/includes/LinkFilter.php b/includes/LinkFilter.php
index dc4c1256..53841df1 100644
--- a/includes/LinkFilter.php
+++ b/includes/LinkFilter.php
@@ -3,7 +3,7 @@
/**
* Some functions to help implement an external link filter for spam control.
*
- * TODO: implement the filter. Currently these are just some functions to help
+ * @todo implement the filter. Currently these are just some functions to help
* maintenance/cleanupSpam.php remove links to a single specified domain. The
* next thing is to implement functions for checking a given page against a big
* list of domains.
@@ -11,8 +11,13 @@
* Another cool thing to do would be a web interface for fast spam removal.
*/
class LinkFilter {
+
/**
- * @static
+ * Check whether $text contains a link to $filterEntry
+ *
+ * @param $text String: text to check
+ * @param $filterEntry String: domainparts, see makeRegex() for more details
+ * @return Integer: 0 if no match or 1 if there's at least one match
*/
static function matchEntry( $text, $filterEntry ) {
$regex = LinkFilter::makeRegex( $filterEntry );
@@ -20,7 +25,11 @@ class LinkFilter {
}
/**
- * @static
+ * Builds a regex pattern for $filterEntry.
+ *
+ * @param $filterEntry String: URL, if it begins with "*.", it'll be
+ * replaced to match any subdomain
+ * @return String: regex pattern, for preg_match()
*/
private static function makeRegex( $filterEntry ) {
$regex = '!http://';
@@ -46,11 +55,47 @@ class LinkFilter {
*
* Asterisks in any other location are considered invalid.
*
- * @static
* @param $filterEntry String: domainparts
* @param $prot String: protocol
+ * @return String
+ * @deprecated Use makeLikeArray() and pass result to Database::buildLike() instead
*/
public static function makeLike( $filterEntry , $prot = 'http://' ) {
+ wfDeprecated( __METHOD__ );
+
+ $like = self::makeLikeArray( $filterEntry , $prot );
+ if ( !$like ) {
+ return false;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $s = $dbw->buildLike( $like );
+ $m = false;
+ if ( preg_match( "/^ *LIKE '(.*)' *$/", $s, $m ) ) {
+ return $m[1];
+ } else {
+ throw new MWException( __METHOD__.': this DBMS is not supported by this function.' );
+ }
+ }
+
+ /**
+ * Make an array to be used for calls to DatabaseBase::buildLike(), which
+ * will match the specified string. There are several kinds of filter entry:
+ * *.domain.com - Produces http://com.domain.%, matches domain.com
+ * and www.domain.com
+ * domain.com - Produces http://com.domain./%, matches domain.com
+ * or domain.com/ but not www.domain.com
+ * *.domain.com/x - Produces http://com.domain.%/x%, matches
+ * www.domain.com/xy
+ * domain.com/x - Produces http://com.domain./x%, matches
+ * domain.com/xy but not www.domain.com/xy
+ *
+ * Asterisks in any other location are considered invalid.
+ *
+ * @param $filterEntry String: domainparts
+ * @param $prot String: protocol
+ * @return Array to be passed to DatabaseBase::buildLike() or false on error
+ */
+ public static function makeLikeArray( $filterEntry , $prot = 'http://' ) {
$db = wfGetDB( DB_MASTER );
if ( substr( $filterEntry, 0, 2 ) == '*.' ) {
$subdomains = true;
@@ -84,25 +129,46 @@ class LinkFilter {
$mailparts = explode( '@', $host );
$domainpart = strtolower( implode( '.', array_reverse( explode( '.', $mailparts[1] ) ) ) );
$host = $domainpart . '@' . $mailparts[0];
- $like = $db->escapeLike( "$prot$host" ) . "%";
+ $like = array( "$prot$host", $db->anyString() );
} elseif ( $prot == 'mailto:' ) {
// domainpart of email adress only. do not add '.'
$host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
- $like = $db->escapeLike( "$prot$host" ) . "%";
+ $like = array( "$prot$host", $db->anyString() );
} else {
$host = strtolower( implode( '.', array_reverse( explode( '.', $host ) ) ) );
if ( substr( $host, -1, 1 ) !== '.' ) {
$host .= '.';
}
- $like = $db->escapeLike( "$prot$host" );
+ $like = array( "$prot$host" );
if ( $subdomains ) {
- $like .= '%';
+ $like[] = $db->anyString();
}
if ( !$subdomains || $path !== '/' ) {
- $like .= $db->escapeLike( $path ) . '%';
+ $like[] = $path;
+ $like[] = $db->anyString();
}
}
return $like;
}
+
+ /**
+ * Filters an array returned by makeLikeArray(), removing everything past first pattern placeholder.
+ *
+ * @param $arr array: array to filter
+ * @return filtered array
+ */
+ public static function keepOneWildcard( $arr ) {
+ if( !is_array( $arr ) ) {
+ return $arr;
+ }
+
+ foreach( $arr as $key => $value ) {
+ if ( $value instanceof LikeMatch ) {
+ return array_slice( $arr, 0, $key + 1 );
+ }
+ }
+
+ return $arr;
+ }
}
diff --git a/includes/Linker.php b/includes/Linker.php
index b739244b..fe193011 100644
--- a/includes/Linker.php
+++ b/includes/Linker.php
@@ -18,24 +18,14 @@ class Linker {
function __construct() {}
/**
- * @deprecated
- */
- function postParseLinkColour( $s = null ) {
- wfDeprecated( __METHOD__ );
- return null;
- }
-
- /**
* 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 getExternalLinkAttributes( $class = 'external' ) {
+ return $this->getLinkAttributesInternal( '', $class );
}
/**
@@ -48,7 +38,7 @@ class Linker {
* @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='' ) {
+ function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
global $wgContLang;
# FIXME: We have a whole bunch of handling here that doesn't happen in
@@ -57,7 +47,7 @@ class Linker {
$title = $wgContLang->checkTitleEncoding( $title );
$title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
- return $this->getLinkAttributesInternal( $title, $class, 'external' );
+ return $this->getLinkAttributesInternal( $title, $class );
}
/**
@@ -95,20 +85,16 @@ class Linker {
/**
* Common code for getLinkAttributesX functions
*/
- private function getLinkAttributesInternal( $title, $class, $classDefault = false ) {
+ private function getLinkAttributesInternal( $title, $class ) {
$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 !== '' ) {
+ if ( $class != '' ) {
$r .= " class=\"$class\"";
}
- $r .= " title=\"$title\"";
+ if ( $title != '') {
+ $r .= " title=\"$title\"";
+ }
return $r;
}
@@ -124,7 +110,7 @@ class Linker {
if ( $t->isRedirect() ) {
# Page is a redirect
$colour = 'mw-redirect';
- } elseif ( $threshold > 0 &&
+ } elseif ( $threshold > 0 &&
$t->exists() && $t->getLength() < $threshold &&
MWNamespace::isContent( $t->getNamespace() ) ) {
# Page is a stub
@@ -220,13 +206,23 @@ class Linker {
$ret = null;
if( wfRunHooks( 'LinkEnd', array( $this, $target, $options, &$text, &$attribs, &$ret ) ) ) {
- $ret = Xml::openElement( 'a', $attribs ) . $text . Xml::closeElement( 'a' );
+ $ret = Html::rawElement( 'a', $attribs, $text );
}
wfProfileOut( __METHOD__ );
return $ret;
}
+ /**
+ * Identical to link(), except $options defaults to 'known'.
+ */
+ public function linkKnown( $target, $text = null, $customAttribs = array(), $query = array(), $options = array('known','noclasses') ) {
+ return $this->link( $target, $text, $customAttribs, $query, $options );
+ }
+
+ /**
+ * Returns the Url used to link to a Title
+ */
private function linkUrl( $target, $query, $options ) {
wfProfileIn( __METHOD__ );
# We don't want to include fragments for broken links, because they
@@ -249,6 +245,9 @@ class Linker {
return $ret;
}
+ /**
+ * Returns the array of attributes used when linking to the Title $target
+ */
private function linkAttribs( $target, $attribs, $options ) {
wfProfileIn( __METHOD__ );
global $wgUser;
@@ -268,7 +267,7 @@ class Linker {
}
# Note that redirects never count as stubs here.
- if ( $target->isRedirect() ) {
+ if ( !in_array( 'broken', $options ) && $target->isRedirect() ) {
$classes[] = 'mw-redirect';
} elseif( $target->isContentPage() ) {
# Check for stub.
@@ -284,7 +283,10 @@ class Linker {
}
# Get a default title attribute.
- if( in_array( 'known', $options ) ) {
+ if( $target->getPrefixedText() == '' ) {
+ # A link like [[#Foo]]. This used to mean an empty title
+ # attribute, but that's silly. Just don't output a title.
+ } elseif( in_array( 'known', $options ) ) {
$defaults['title'] = $target->getPrefixedText();
} else {
$defaults['title'] = wfMsg( 'red-link-title', $target->getPrefixedText() );
@@ -305,6 +307,9 @@ class Linker {
return $ret;
}
+ /**
+ * Default text of the links to the Title $target
+ */
private function linkText( $target ) {
# We might be passed a non-Title by make*LinkObj(). Fail gracefully.
if( !$target instanceof Title ) {
@@ -320,236 +325,6 @@ class Linker {
}
/**
- * @deprecated Use link()
- *
- * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call
- * it if you already have a title object handy. See makeLinkObj for further documentation.
- *
- * @param $title String: the text of the title
- * @param $text String: link text
- * @param $query String: optional query part
- * @param $trail String: optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeLink( $title, $text = '', $query = '', $trail = '' ) {
- wfProfileIn( __METHOD__ );
- $nt = Title::newFromText( $title );
- if ( $nt instanceof Title ) {
- $result = $this->makeLinkObj( $nt, $text, $query, $trail );
- } else {
- wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" );
- $result = $text == "" ? $title : $text;
- }
-
- wfProfileOut( __METHOD__ );
- return $result;
- }
-
- /**
- * @deprecated Use link()
- *
- * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call
- * it if you already have a title object handy. See makeKnownLinkObj for further documentation.
- *
- * @param $title String: the text of the title
- * @param $text String: link text
- * @param $query String: optional query part
- * @param $trail String: optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') {
- $nt = Title::newFromText( $title );
- if ( $nt instanceof Title ) {
- return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix , $aprops );
- } else {
- wfDebug( 'Invalid title passed to Linker::makeKnownLink(): "'.$title."\"\n" );
- return $text == '' ? $title : $text;
- }
- }
-
- /**
- * @deprecated Use link()
- *
- * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call
- * it if you already have a title object handy. See makeBrokenLinkObj for further documentation.
- *
- * @param string $title The text of the title
- * @param string $text Link text
- * @param string $query Optional query part
- * @param string $trail Optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) {
- $nt = Title::newFromText( $title );
- if ( $nt instanceof Title ) {
- return $this->makeBrokenLinkObj( $nt, $text, $query, $trail );
- } else {
- wfDebug( 'Invalid title passed to Linker::makeBrokenLink(): "'.$title."\"\n" );
- return $text == '' ? $title : $text;
- }
- }
-
- /**
- * @deprecated Use link()
- *
- * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call
- * it if you already have a title object handy. See makeStubLinkObj for further documentation.
- *
- * @param $title String: the text of the title
- * @param $text String: link text
- * @param $query String: optional query part
- * @param $trail String: optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeStubLink( $title, $text = '', $query = '', $trail = '' ) {
- wfDeprecated( __METHOD__ );
- $nt = Title::newFromText( $title );
- if ( $nt instanceof Title ) {
- return $this->makeStubLinkObj( $nt, $text, $query, $trail );
- } else {
- wfDebug( 'Invalid title passed to Linker::makeStubLink(): "'.$title."\"\n" );
- return $text == '' ? $title : $text;
- }
- }
-
- /**
- * @deprecated Use link()
- *
- * Make a link for a title which may or may not be in the database. If you need to
- * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each
- * call to this will result in a DB query.
- *
- * @param $nt Title: the title object to make the link from, e.g. from
- * Title::newFromText.
- * @param $text String: link text
- * @param $query String: optional query part
- * @param $trail String: optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- * @param $prefix String: optional prefix. As trail, only before instead of after.
- */
- function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
- global $wgUser;
- wfProfileIn( __METHOD__ );
-
- $query = wfCgiToArray( $query );
- list( $inside, $trail ) = Linker::splitTrail( $trail );
- if( $text === '' ) {
- $text = $this->linkText( $nt );
- }
-
- $ret = $this->link( $nt, "$prefix$text$inside", array(), $query ) . $trail;
-
- wfProfileOut( __METHOD__ );
- return $ret;
- }
-
- /**
- * @deprecated Use link()
- *
- * Make a link for a title which definitely exists. This is faster than makeLinkObj because
- * it doesn't have to do a database query. It's also valid for interwiki titles and special
- * pages.
- *
- * @param $nt Title object of target page
- * @param $text String: text to replace the title
- * @param $query String: link target
- * @param $trail String: text after link
- * @param $prefix String: text before link text
- * @param $aprops String: extra attributes to the a-element
- * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead
- * @return the a-element
- */
- function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
- wfProfileIn( __METHOD__ );
-
- if ( $text == '' ) {
- $text = $this->linkText( $title );
- }
- $attribs = Sanitizer::mergeAttributes(
- Sanitizer::decodeTagAttributes( $aprops ),
- Sanitizer::decodeTagAttributes( $style )
- );
- $query = wfCgiToArray( $query );
- list( $inside, $trail ) = Linker::splitTrail( $trail );
-
- $ret = $this->link( $title, "$prefix$text$inside", $attribs, $query,
- array( 'known', 'noclasses' ) ) . $trail;
-
- wfProfileOut( __METHOD__ );
- return $ret;
- }
-
- /**
- * @deprecated Use link()
- *
- * Make a red link to the edit page of a given title.
- *
- * @param $nt Title object of the target page
- * @param $text String: Link text
- * @param $query String: Optional query part
- * @param $trail String: Optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
- wfProfileIn( __METHOD__ );
-
- list( $inside, $trail ) = Linker::splitTrail( $trail );
- if( $text === '' ) {
- $text = $this->linkText( $title );
- }
- $nt = $this->normaliseSpecialPage( $title );
-
- $ret = $this->link( $title, "$prefix$text$inside", array(),
- wfCgiToArray( $query ), 'broken' ) . $trail;
-
- wfProfileOut( __METHOD__ );
- return $ret;
- }
-
- /**
- * @deprecated Use link()
- *
- * 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
- * @param $trail String: optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- wfDeprecated( __METHOD__ );
- return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix );
- }
-
- /**
- * @deprecated Use link()
- *
- * Make a coloured link.
- *
- * @param $nt Title object of the target page
- * @param $colour Integer: colour of the link
- * @param $text String: link text
- * @param $query String: optional query part
- * @param $trail String: optional trail. Alphabetic characters at the start of this string will
- * be included in the link text. Other characters will be appended after
- * the end of the link.
- */
- function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
- if($colour != ''){
- $style = $this->getInternalLinkAttributesObj( $nt, $text, $colour );
- } else $style = '';
- return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix, '', $style );
- }
-
- /**
* Generate either a normal exists-style link or a stub link, depending
* on the given page size.
*
@@ -565,6 +340,7 @@ class Linker {
global $wgUser;
$threshold = intval( $wgUser->getOption( 'stubthreshold' ) );
$colour = ( $size < $threshold ) ? 'stub' : '';
+ // FIXME: replace deprecated makeColouredLinkObj by link()
return $this->makeColouredLinkObj( $nt, $colour, $text, $query, $trail, $prefix );
}
@@ -574,7 +350,7 @@ class Linker {
* despite $query not being used.
*/
function makeSelfLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
- if ( '' == $text ) {
+ if ( $text == '' ) {
$text = htmlspecialchars( $nt->getPrefixedText() );
}
list( $inside, $trail ) = Linker::splitTrail( $trail );
@@ -593,7 +369,10 @@ class Linker {
}
}
- /** @todo document */
+ /**
+ * Returns the filename part of an url.
+ * Used as alternative text for external images.
+ */
function fnamePart( $url ) {
$basename = strrchr( $url, '/' );
if ( false === $basename ) {
@@ -604,15 +383,12 @@ class Linker {
return $basename;
}
- /** Obsolete alias */
- function makeImage( $url, $alt = '' ) {
- wfDeprecated( __METHOD__ );
- return $this->makeExternalImage( $url, $alt );
- }
-
- /** @todo document */
+ /**
+ * Return the code for images which were added via external links,
+ * via Parser::maybeMakeExternalImage().
+ */
function makeExternalImage( $url, $alt = '' ) {
- if ( '' == $alt ) {
+ if ( $alt == '' ) {
$alt = $this->fnamePart( $url );
}
$img = '';
@@ -621,52 +397,13 @@ class Linker {
wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}\n", true);
return $img;
}
- return Xml::element( 'img',
+ return Html::element( 'img',
array(
'src' => $url,
'alt' => $alt ) );
}
/**
- * Creates the HTML source for images
- * @deprecated use makeImageLink2
- *
- * @param object $title
- * @param string $label label text
- * @param string $alt alt text
- * @param string $align horizontal alignment: none, left, center, right)
- * @param array $handlerParams Parameters to be passed to the media handler
- * @param boolean $framed shows image in original size in a frame
- * @param boolean $thumb shows image as thumbnail in a frame
- * @param string $manualthumb image name for the manual thumbnail
- * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom
- * @param string $time, timestamp of the file, set as false for current
- * @return string
- */
- function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false,
- $thumb = false, $manualthumb = '', $valign = '', $time = false )
- {
- $frameParams = array( 'alt' => $alt, 'caption' => $label );
- if ( $align ) {
- $frameParams['align'] = $align;
- }
- if ( $framed ) {
- $frameParams['framed'] = true;
- }
- if ( $thumb ) {
- $frameParams['thumbnail'] = true;
- }
- if ( $manualthumb ) {
- $frameParams['manualthumb'] = $manualthumb;
- }
- if ( $valign ) {
- $frameParams['valign'] = $valign;
- }
- $file = wfFindFile( $title, $time );
- return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams, $time );
- }
-
- /**
* Given parameters derived from [[Image:Foo|options...]], generate the
* HTML that that syntax inserts in the page.
*
@@ -719,8 +456,7 @@ class Linker {
$page = isset( $hp['page'] ) ? $hp['page'] : false;
if ( !isset( $fp['align'] ) ) $fp['align'] = '';
if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
- # Backward compatibility, title used to always be equal to alt text
- if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt'];
+ if ( !isset( $fp['title'] ) ) $fp['title'] = '';
$prefix = $postfix = '';
@@ -763,7 +499,7 @@ class Linker {
# If thumbnail width has not been provided, it is set
# to the default user option as specified in Language*.php
if ( $fp['align'] == '' ) {
- $fp['align'] = $wgContLang->isRTL() ? 'left' : 'right';
+ $fp['align'] = $wgContLang->alignEnd();
}
return $prefix.$this->makeThumbLink2( $title, $file, $fp, $hp, $time, $query ).$postfix;
}
@@ -785,7 +521,7 @@ class Linker {
}
if ( !$thumb ) {
- $s = $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true );
+ $s = $this->makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time==true );
} else {
$params = array(
'alt' => $fp['alt'],
@@ -805,7 +541,7 @@ class Linker {
$s = $thumb->toHtml( $params );
}
- if ( '' != $fp['align'] ) {
+ if ( $fp['align'] != '' ) {
$s = "<div class=\"float{$fp['align']}\">{$s}</div>";
}
return str_replace("\n", ' ',$prefix.$s.$postfix);
@@ -838,8 +574,7 @@ class Linker {
$page = isset( $hp['page'] ) ? $hp['page'] : false;
if ( !isset( $fp['align'] ) ) $fp['align'] = 'right';
if ( !isset( $fp['alt'] ) ) $fp['alt'] = '';
- # Backward compatibility, title used to always be equal to alt text
- if ( !isset( $fp['title'] ) ) $fp['title'] = $fp['alt'];
+ if ( !isset( $fp['title'] ) ) $fp['title'] = '';
if ( !isset( $fp['caption'] ) ) $fp['caption'] = '';
if ( empty( $hp['width'] ) ) {
@@ -886,7 +621,7 @@ class Linker {
# So we don't need to pass it here in $query. However, the URL for the
# zoom icon still needs it, so we make a unique query for it. See bug 14771
$url = $title->getLocalURL( $query );
- if( $page ) {
+ if( $page ) {
$url = wfAppendQuery( $url, 'page=' . urlencode( $page ) );
}
@@ -894,7 +629,7 @@ class Linker {
$s = "<div class=\"thumb t{$fp['align']}\"><div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
if( !$exists ) {
- $s .= $this->makeBrokenImageLinkObj( $title, '', '', '', '', $time==true );
+ $s .= $this->makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time==true );
$zoomicon = '';
} elseif ( !$thumb ) {
$s .= htmlspecialchars( wfMsg( 'thumbnail_error', '' ) );
@@ -931,26 +666,31 @@ class Linker {
* @return string
*/
public function makeBrokenImageLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '', $time = false ) {
- global $wgEnableUploads;
+ global $wgEnableUploads, $wgUploadNavigationUrl;
if( $title instanceof Title ) {
wfProfileIn( __METHOD__ );
$currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
- if( $wgEnableUploads && !$currentExists ) {
- $upload = SpecialPage::getTitleFor( 'Upload' );
+ if( ( $wgUploadNavigationUrl || $wgEnableUploads ) && !$currentExists ) {
if( $text == '' )
$text = htmlspecialchars( $title->getPrefixedText() );
+
$redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
if( $redir ) {
+ wfProfileOut( __METHOD__ );
return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix );
}
- $q = 'wpDestFile=' . $title->getPartialUrl();
- if( $query != '' )
- $q .= '&' . $query;
+
+ $href = $this->getUploadUrl( $title, $query );
+
+
list( $inside, $trail ) = self::splitTrail( $trail );
- $style = $this->getInternalLinkAttributesObj( $title, $text, 'new' );
+
wfProfileOut( __METHOD__ );
- return '<a href="' . $upload->escapeLocalUrl( $q ) . '"'
- . $style . '>' . $prefix . $text . $inside . '</a>' . $trail;
+ return Html::element( 'a', array(
+ 'href' => $href,
+ 'class' => 'new',
+ 'title' => $title->getPrefixedText()
+ ), $prefix . $text . $inside ) . $trail;
} else {
wfProfileOut( __METHOD__ );
return $this->makeKnownLinkObj( $title, $text, $query, $trail, $prefix );
@@ -959,11 +699,26 @@ class Linker {
return "<!-- ERROR -->{$prefix}{$text}{$trail}";
}
}
-
- /** @deprecated use Linker::makeMediaLinkObj() */
- function makeMediaLink( $name, $unused = '', $text = '', $time = false ) {
- $nt = Title::makeTitleSafe( NS_FILE, $name );
- return $this->makeMediaLinkObj( $nt, $text, $time );
+
+ /**
+ * Get the URL to upload a certain file
+ *
+ * @param $destFile Title Title of the file to upload
+ * @param $query string Urlencoded query string to prepend
+ * @return string Urlencoded URL
+ */
+ protected function getUploadUrl( $destFile, $query = '' ) {
+ global $wgUploadNavigationUrl;
+ $q = 'wpDestFile=' . $destFile->getPartialUrl();
+ if( $query != '' )
+ $q .= '&' . $query;
+
+ if( $wgUploadNavigationUrl ) {
+ return wfAppendQuery( $wgUploadNavigationUrl, $q );
+ } else {
+ $upload = SpecialPage::getTitleFor( 'Upload' );
+ return $upload->getLocalUrl( $q );
+ }
}
/**
@@ -982,13 +737,12 @@ class Linker {
### HOTFIX. Instead of breaking, return empty string.
return $text;
} else {
- $img = wfFindFile( $title, $time );
+ $img = wfFindFile( $title, array( 'time' => $time ) );
if( $img ) {
$url = $img->getURL();
$class = 'internal';
} else {
- $upload = SpecialPage::getTitleFor( 'Upload' );
- $url = $upload->getLocalUrl( 'wpDestFile=' . urlencode( $title->getDBkey() ) );
+ $url = $this->getUploadUrl( $title );
$class = 'new';
}
$alt = htmlspecialchars( $title->getText() );
@@ -1000,11 +754,15 @@ class Linker {
}
}
- /** @todo document */
+ /**
+ * Make a link to a special page given its name and, optionally,
+ * a message key from the link text.
+ * Usage example: $skin->specialLink( 'recentchanges' )
+ */
function specialLink( $name, $key = '' ) {
global $wgContLang;
- if ( '' == $key ) { $key = strtolower( $name ); }
+ if ( $key == '' ) { $key = strtolower( $name ); }
$pn = $wgContLang->ucfirst( $name );
return $this->makeKnownLink( $wgContLang->specialPage( $pn ),
wfMsg( $key ) );
@@ -1017,17 +775,20 @@ class Linker {
* @param boolean $escape Do we escape the link text?
* @param String $linktype Type of external link. Gets added to the classes
* @param array $attribs Array of extra attributes to <a>
- *
- * @TODO! @FIXME! This is a really crappy implementation. $linktype and
+ *
+ * @todo FIXME: This is a really crappy implementation. $linktype and
* 'external' are mashed into the class attrib for the link (which is made
- * into a string). Then, if we've got additional params in $attribs, we
+ * into a string). Then, if we've got additional params in $attribs, we
* add to it. People using this might want to change the classes (or other
- * default link attributes), but passing $attribsText is just messy. Would
- * make a lot more sense to make put the classes into $attribs, let the
- * hook play with them, *then* expand it all at once.
+ * default link attributes), but passing $attribsText is just messy. Would
+ * make a lot more sense to make put the classes into $attribs, let the
+ * hook play with them, *then* expand it all at once.
*/
function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) {
- $attribsText = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype );
+ if ( isset( $attribs[ 'class' ] ) ) $class = $attribs[ 'class' ]; # yet another hack :(
+ else $class = 'external ' . $linktype;
+
+ $attribsText = $this->getExternalLinkAttributes( $class );
$url = htmlspecialchars( $url );
if( $escape ) {
$text = htmlspecialchars( $text );
@@ -1039,7 +800,7 @@ class Linker {
return $link;
}
if ( $attribs ) {
- $attribsText .= Xml::expandAttributes( $attribs );
+ $attribsText .= Html::expandAttributes( $attribs );
}
return '<a href="'.$url.'"'.$attribsText.'>'.$text.'</a>';
}
@@ -1148,7 +909,7 @@ class Linker {
if( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
$link = wfMsgHtml( 'rev-deleted-user' );
} else if( $rev->userCan( Revision::DELETED_USER ) ) {
- $link = $this->userLink( $rev->getUser( Revision::FOR_THIS_USER ),
+ $link = $this->userLink( $rev->getUser( Revision::FOR_THIS_USER ),
$rev->getUserText( Revision::FOR_THIS_USER ) );
} else {
$link = wfMsgHtml( 'rev-deleted-user' );
@@ -1170,7 +931,7 @@ class Linker {
$link = wfMsgHtml( 'rev-deleted-user' );
} else if( $rev->userCan( Revision::DELETED_USER ) ) {
$userId = $rev->getUser( Revision::FOR_THIS_USER );
- $userText = $rev->getUserText( Revision::FOR_THIS_USER );
+ $userText = $rev->getUserText( Revision::FOR_THIS_USER );
$link = $this->userLink( $userId, $userText ) .
' ' . $this->userToolLinks( $userId, $userText );
} else {
@@ -1198,7 +959,7 @@ class Linker {
* @param mixed $title Title object (to generate link to the section in autocomment) or null
* @param bool $local Whether section links should refer to local page
*/
- function formatComment($comment, $title = NULL, $local = false) {
+ function formatComment($comment, $title = null, $local = false) {
wfProfileIn( __METHOD__ );
# Sanitize text a bit:
@@ -1207,8 +968,8 @@ class Linker {
$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
# Render autocomments and make links:
- $comment = $this->formatAutoComments( $comment, $title, $local );
- $comment = $this->formatLinksInComment( $comment );
+ $comment = $this->formatAutocomments( $comment, $title, $local );
+ $comment = $this->formatLinksInComment( $comment, $title, $local );
wfProfileOut( __METHOD__ );
return $comment;
@@ -1239,16 +1000,16 @@ class Linker {
unset( $this->autocommentLocal );
return $comment;
}
-
+
private function formatAutocommentsCallback( $match ) {
$title = $this->autocommentTitle;
$local = $this->autocommentLocal;
-
- $pre=$match[1];
- $auto=$match[2];
- $post=$match[3];
- $link='';
- if( $title ) {
+
+ $pre = $match[1];
+ $auto = $match[2];
+ $post = $match[3];
+ $link = '';
+ if ( $title ) {
$section = $auto;
# Generate a valid anchor name from the section title.
@@ -1262,12 +1023,12 @@ class Linker {
if ( $local ) {
$sectionTitle = Title::newFromText( '#' . $section );
} else {
- $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
+ $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
$title->getDBkey(), $section );
}
if ( $sectionTitle ) {
$link = $this->link( $sectionTitle,
- wfMsgForContent( 'sectionlink' ), array(), array(),
+ htmlspecialchars( wfMsgForContent( 'sectionlink' ) ), array(), array(),
'noclasses' );
} else {
$link = '';
@@ -1291,15 +1052,20 @@ class Linker {
* Formats wiki links and media links in text; all other wiki formatting
* is ignored
*
- * @fixme doesn't handle sub-links as in image thumb texts like the main parser
+ * @todo Fixme: doesn't handle sub-links as in image thumb texts like the main parser
* @param string $comment Text to format links in
* @return string
*/
- public function formatLinksInComment( $comment ) {
- return preg_replace_callback(
+ public function formatLinksInComment( $comment, $title = null, $local = false ) {
+ $this->commentContextTitle = $title;
+ $this->commentLocal = $local;
+ $html = preg_replace_callback(
'/\[\[:?(.*?)(\|(.*?))*\]\]([^[]*)/',
array( $this, 'formatLinksInCommentCallback' ),
$comment );
+ unset( $this->commentContextTitle );
+ unset( $this->commentLocal );
+ return $html;
}
protected function formatLinksInCommentCallback( $match ) {
@@ -1316,16 +1082,18 @@ class Linker {
}
# Handle link renaming [[foo|text]] will show link as "text"
- if( "" != $match[3] ) {
+ if( $match[3] != "" ) {
$text = $match[3];
} else {
$text = $match[1];
}
$submatch = array();
+ $thelink = null;
if( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
# Media link; trail not supported.
$linkRegexp = '/\[\[(.*?)\]\]/';
- $thelink = $this->makeMediaLink( $submatch[1], "", $text );
+ $title = Title::makeTitleSafe( NS_FILE, $submatch[1] );
+ $thelink = $this->makeMediaLinkObj( $title, $text );
} else {
# Other kind of link
if( preg_match( $wgContLang->linkTrail(), $match[4], $submatch ) ) {
@@ -1336,13 +1104,105 @@ class Linker {
$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
if (isset($match[1][0]) && $match[1][0] == ':')
$match[1] = substr($match[1], 1);
- $thelink = $this->makeLink( $match[1], $text, "", $trail );
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+
+ $linkText = $text;
+ $linkTarget = Linker::normalizeSubpageLink( $this->commentContextTitle,
+ $match[1], $linkText );
+
+ $target = Title::newFromText( $linkTarget );
+ if( $target ) {
+ if( $target->getText() == '' && !$this->commentLocal && $this->commentContextTitle ) {
+ $newTarget = clone( $this->commentContextTitle );
+ $newTarget->setFragment( '#' . $target->getFragment() );
+ $target = $newTarget;
+ }
+ $thelink = $this->link(
+ $target,
+ $linkText . $inside
+ ) . $trail;
+ }
+ }
+ if( $thelink ) {
+ // If the link is still valid, go ahead and replace it in!
+ $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 );
}
- $comment = preg_replace( $linkRegexp, StringUtils::escapeRegexReplacement( $thelink ), $comment, 1 );
return $comment;
}
+ static function normalizeSubpageLink( $contextTitle, $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
+
+ wfProfileIn( __METHOD__ );
+ $ret = $target; # default return value is no change
+
+ # Some namespaces don't allow subpages,
+ # so only perform processing if subpages are allowed
+ if( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
+ $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 = $contextTitle->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( '/', $contextTitle->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( __METHOD__ );
+ return $ret;
+ }
+
/**
* Wrap a comment in standard punctuation and formatting if
* it's non-empty, otherwise return empty string.
@@ -1353,7 +1213,7 @@ class Linker {
*
* @return string
*/
- function commentBlock( $comment, $title = NULL, $local = false ) {
+ function commentBlock( $comment, $title = null, $local = false ) {
// '*' used to be the comment inserted by the software way back
// in antiquity in case none was provided, here for backwards
// compatability, acc. to brion -ævar
@@ -1375,6 +1235,7 @@ class Linker {
* @return string HTML
*/
function revComment( Revision $rev, $local = false, $isPublic = false ) {
+ if( $rev->getRawComment() == "" ) return "";
if( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
$block = " <span class=\"comment\">" . wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
} else if( $rev->userCan( Revision::DELETED_COMMENT ) ) {
@@ -1401,12 +1262,16 @@ class Linker {
return "<span class=\"history-size\">$stxt</span>";
}
- /** @todo document */
+ /**
+ * Add another level to the Table of Contents
+ */
function tocIndent() {
return "\n<ul>";
}
- /** @todo document */
+ /**
+ * Finish one or more sublevels on the Table of Contents
+ */
function tocUnindent($level) {
return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level>0 ? $level : 0 );
}
@@ -1414,64 +1279,73 @@ class Linker {
/**
* parameter level defines if we are on an indentation level
*/
- function tocLine( $anchor, $tocline, $tocnumber, $level ) {
- return "\n<li class=\"toclevel-$level\"><a href=\"#" .
+ function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
+ $classes = "toclevel-$level";
+ if ( $sectionIndex !== false )
+ $classes .= " tocsection-$sectionIndex";
+ return "\n<li class=\"$classes\"><a href=\"#" .
$anchor . '"><span class="tocnumber">' .
$tocnumber . '</span> <span class="toctext">' .
$tocline . '</span></a>';
}
- /** @todo document */
+ /**
+ * End a Table Of Contents line.
+ * tocUnindent() will be used instead if we're ending a line below
+ * the new level.
+ */
function tocLineEnd() {
return "</li>\n";
}
- /** @todo document */
+ /**
+ * Wraps the TOC in a table and provides the hide/collapse javascript.
+ * @param string $toc html of the Table Of Contents
+ * @return string Full html of the TOC
+ */
function tocList($toc) {
- global $wgJsMimeType;
$title = wfMsgHtml('toc') ;
return
- '<table id="toc" class="toc" summary="' . $title .'"><tr><td>'
+ '<table id="toc" class="toc"><tr><td>'
. '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
. $toc
# no trailing newline, script should not be wrapped in a
# paragraph
. "</ul>\n</td></tr></table>"
- . '<script type="' . $wgJsMimeType . '">'
- . ' if (window.showTocToggle) {'
- . ' var tocShowText = "' . Xml::escapeJsString( wfMsg('showtoc') ) . '";'
- . ' var tocHideText = "' . Xml::escapeJsString( wfMsg('hidetoc') ) . '";'
- . ' showTocToggle();'
- . ' } '
- . "</script>\n";
- }
-
- /**
- * Used to generate section edit links that point to "other" pages
- * (sections that are really part of included pages).
- *
- * @param $title Title string.
- * @param $section Integer: section number.
- */
- public function editSectionLinkForOther( $title, $section ) {
- wfDeprecated( __METHOD__ );
- $title = Title::newFromText( $title );
- return $this->doEditSectionLink( $title, $section );
+ . Html::inlineScript(
+ 'if (window.showTocToggle) {'
+ . ' var tocShowText = "' . Xml::escapeJsString( wfMsg('showtoc') ) . '";'
+ . ' var tocHideText = "' . Xml::escapeJsString( wfMsg('hidetoc') ) . '";'
+ . ' showTocToggle();'
+ . ' } ' )
+ . "\n";
}
/**
- * @param $nt Title object.
- * @param $section Integer: section number.
- * @param $hint Link String: title, or default if omitted or empty
+ * Generate a table of contents from a section tree
+ * Currently unused.
+ * @param $tree Return value of ParserOutput::getSections()
+ * @return string HTML
*/
- public function editSectionLink( Title $nt, $section, $hint = '' ) {
- wfDeprecated( __METHOD__ );
- if( $hint === '' ) {
- # No way to pass an actual empty $hint here! The new interface al-
- # lows this, so we have to do this for compatibility.
- $hint = null;
- }
- return $this->doEditSectionLink( $nt, $section, $hint );
+ public function generateTOC( $tree ) {
+ $toc = '';
+ $lastLevel = 0;
+ foreach ( $tree as $section ) {
+ if ( $section['toclevel'] > $lastLevel )
+ $toc .= $this->tocIndent();
+ else if ( $section['toclevel'] < $lastLevel )
+ $toc .= $this->tocUnindent(
+ $lastLevel - $section['toclevel'] );
+ else
+ $toc .= $this->tocLineEnd();
+
+ $toc .= $this->tocLine( $section['anchor'],
+ $section['line'], $section['number'],
+ $section['toclevel'], $section['index'] );
+ $lastLevel = $section['toclevel'];
+ }
+ $toc .= $this->tocLineEnd();
+ return $this->tocList( $toc );
}
/**
@@ -1487,6 +1361,8 @@ class Linker {
* @return string HTML to use for edit link
*/
public function doEditSectionLink( Title $nt, $section, $tooltip = null ) {
+ // HTML generated here should probably have userlangattributes
+ // added to it for LTR text on RTL pages
$attribs = array();
if( !is_null( $tooltip ) ) {
$attribs['title'] = wfMsg( 'editsectionhint', $tooltip );
@@ -1539,13 +1415,12 @@ class Linker {
* @return string HTML headline
*/
public function makeHeadline( $level, $attribs, $anchor, $text, $link, $legacyAnchor = false ) {
- $ret = "<a name=\"$anchor\" id=\"$anchor\"></a>"
- . "<h$level$attribs"
+ $ret = "<h$level$attribs"
. $link
- . " <span class=\"mw-headline\">$text</span>"
+ . " <span class=\"mw-headline\" id=\"$anchor\">$text</span>"
. "</h$level>";
if ( $legacyAnchor !== false ) {
- $ret = "<a name=\"$legacyAnchor\" id=\"$legacyAnchor\"></a>$ret";
+ $ret = "<a id=\"$legacyAnchor\"></a>$ret";
}
return $ret;
}
@@ -1563,7 +1438,7 @@ class Linker {
$regex = $wgContLang->linkTrail();
}
$inside = '';
- if ( '' != $trail ) {
+ if ( $trail != '' ) {
$m = array();
if ( preg_match( $regex, $trail, $m ) ) {
$inside = $m[1];
@@ -1640,11 +1515,11 @@ class Linker {
# Construct the HTML
$outText = '<div class="mw-templatesUsedExplanation">';
if ( $preview ) {
- $outText .= wfMsgExt( 'templatesusedpreview', array( 'parse' ) );
+ $outText .= wfMsgExt( 'templatesusedpreview', array( 'parse' ), count( $templates ) );
} elseif ( $section ) {
- $outText .= wfMsgExt( 'templatesusedsection', array( 'parse' ) );
+ $outText .= wfMsgExt( 'templatesusedsection', array( 'parse' ), count( $templates ) );
} else {
- $outText .= wfMsgExt( 'templatesused', array( 'parse' ) );
+ $outText .= wfMsgExt( 'templatesused', array( 'parse' ), count( $templates ) );
}
$outText .= "</div><ul>\n";
@@ -1659,9 +1534,19 @@ class Linker {
$protected = '';
}
if( $titleObj->quickUserCan( 'edit' ) ) {
- $editLink = $this->makeLinkObj( $titleObj, wfMsg('editlink'), 'action=edit' );
+ $editLink = $this->link(
+ $titleObj,
+ wfMsg( 'editlink' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
} else {
- $editLink = $this->makeLinkObj( $titleObj, wfMsg('viewsourcelink'), 'action=edit' );
+ $editLink = $this->link(
+ $titleObj,
+ wfMsg( 'viewsourcelink' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
}
$outText .= '<li>' . $this->link( $titleObj ) . ' (' . $editLink . ') ' . $protected . '</li>';
}
@@ -1711,40 +1596,6 @@ class Linker {
}
/**
- * @deprecated Returns raw bits of HTML, use titleAttrib() and accesskey()
- */
- public function tooltipAndAccesskey( $name ) {
- # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
- # no attribute" instead of "output '' as value for attribute", this
- # would be three lines.
- $attribs = array(
- 'title' => $this->titleAttrib( $name, 'withaccess' ),
- 'accesskey' => $this->accesskey( $name )
- );
- if ( $attribs['title'] === false ) {
- unset( $attribs['title'] );
- }
- if ( $attribs['accesskey'] === false ) {
- unset( $attribs['accesskey'] );
- }
- return Xml::expandAttributes( $attribs );
- }
-
- /** @deprecated Returns raw bits of HTML, use titleAttrib() */
- public function tooltip( $name, $options = null ) {
- # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
- # no attribute" instead of "output '' as value for attribute", this
- # would be two lines.
- $tooltip = $this->titleAttrib( $name, $options );
- if ( $tooltip === false ) {
- return '';
- }
- return Xml::expandAttributes( array(
- 'title' => $this->titleAttrib( $name, $options )
- ) );
- }
-
- /**
* Given the id of an interface element, constructs the appropriate title
* attribute from the system messages. (Note, this is usually the id but
* isn't always, because sometimes the accesskey needs to go on a different
@@ -1809,21 +1660,403 @@ class Linker {
wfProfileOut( __METHOD__ );
return false;
}
-
+
/**
* Creates a (show/hide) link for deleting revisions/log entries
*
* @param array $query Query parameters to be passed to link()
* @param bool $restricted Set to true to use a <strong> instead of a <span>
+ * @param bool $delete Set to true to use (show/hide) rather than (show)
*
* @return string HTML <a> link to Special:Revisiondelete, wrapped in a
* span to allow for customization of appearance with CSS
*/
- public function revDeleteLink( $query = array(), $restricted = false ) {
+ public function revDeleteLink( $query = array(), $restricted = false, $delete = true ) {
$sp = SpecialPage::getTitleFor( 'Revisiondelete' );
- $text = wfMsgHtml( 'rev-delundel' );
+ $text = $delete ? wfMsgHtml( 'rev-delundel' ) : wfMsgHtml( 'rev-showdeleted' );
$tag = $restricted ? 'strong' : 'span';
$link = $this->link( $sp, $text, array(), $query, array( 'known', 'noclasses' ) );
return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), "($link)" );
}
+
+ /**
+ * Creates a dead (show/hide) link for deleting revisions/log entries
+ *
+ * @param bool $delete Set to true to use (show/hide) rather than (show)
+ *
+ * @return string HTML text wrapped in a span to allow for customization
+ * of appearance with CSS
+ */
+ public function revDeleteLinkDisabled( $delete = true ) {
+ $text = $delete ? wfMsgHtml( 'rev-delundel' ) : wfMsgHtml( 'rev-showdeleted' );
+ return Xml::tags( 'span', array( 'class' => 'mw-revdelundel-link' ), "($text)" );
+ }
+
+ /* Deprecated methods */
+
+ /**
+ * @deprecated
+ */
+ function postParseLinkColour( $s = null ) {
+ wfDeprecated( __METHOD__ );
+ return null;
+ }
+
+
+ /**
+ * @deprecated Use link()
+ *
+ * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeLinkObj for further documentation.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeLink( $title, $text = '', $query = '', $trail = '' ) {
+ wfProfileIn( __METHOD__ );
+ $nt = Title::newFromText( $title );
+ if ( $nt instanceof Title ) {
+ $result = $this->makeLinkObj( $nt, $text, $query, $trail );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeLink(): "'.$title."\"\n" );
+ $result = $text == "" ? $title : $text;
+ }
+
+ wfProfileOut( __METHOD__ );
+ return $result;
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * This function is a shortcut to makeKnownLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeKnownLinkObj for further documentation.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeKnownLink( $title, $text = '', $query = '', $trail = '', $prefix = '',$aprops = '') {
+ $nt = Title::newFromText( $title );
+ if ( $nt instanceof Title ) {
+ return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix , $aprops );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeKnownLink(): "'.$title."\"\n" );
+ return $text == '' ? $title : $text;
+ }
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * This function is a shortcut to makeBrokenLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeBrokenLinkObj for further documentation.
+ *
+ * @param string $title The text of the title
+ * @param string $text Link text
+ * @param string $query Optional query part
+ * @param string $trail Optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeBrokenLink( $title, $text = '', $query = '', $trail = '' ) {
+ $nt = Title::newFromText( $title );
+ if ( $nt instanceof Title ) {
+ return $this->makeBrokenLinkObj( $nt, $text, $query, $trail );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeBrokenLink(): "'.$title."\"\n" );
+ return $text == '' ? $title : $text;
+ }
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * This function is a shortcut to makeStubLinkObj(Title::newFromText($title),...). Do not call
+ * it if you already have a title object handy. See makeStubLinkObj for further documentation.
+ *
+ * @param $title String: the text of the title
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeStubLink( $title, $text = '', $query = '', $trail = '' ) {
+ wfDeprecated( __METHOD__ );
+ $nt = Title::newFromText( $title );
+ if ( $nt instanceof Title ) {
+ return $this->makeStubLinkObj( $nt, $text, $query, $trail );
+ } else {
+ wfDebug( 'Invalid title passed to Linker::makeStubLink(): "'.$title."\"\n" );
+ return $text == '' ? $title : $text;
+ }
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * Make a link for a title which may or may not be in the database. If you need to
+ * call this lots of times, pre-fill the link cache with a LinkBatch, otherwise each
+ * call to this will result in a DB query.
+ *
+ * @param $nt Title: the title object to make the link from, e.g. from
+ * Title::newFromText.
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ * @param $prefix String: optional prefix. As trail, only before instead of after.
+ */
+ function makeLinkObj( $nt, $text= '', $query = '', $trail = '', $prefix = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ $query = wfCgiToArray( $query );
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ if( $text === '' ) {
+ $text = $this->linkText( $nt );
+ }
+
+ $ret = $this->link( $nt, "$prefix$text$inside", array(), $query ) . $trail;
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * Make a link for a title which definitely exists. This is faster than makeLinkObj because
+ * it doesn't have to do a database query. It's also valid for interwiki titles and special
+ * pages.
+ *
+ * @param $nt Title object of target page
+ * @param $text String: text to replace the title
+ * @param $query String: link target
+ * @param $trail String: text after link
+ * @param $prefix String: text before link text
+ * @param $aprops String: extra attributes to the a-element
+ * @param $style String: style to apply - if empty, use getInternalLinkAttributesObj instead
+ * @return the a-element
+ */
+ function makeKnownLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' , $aprops = '', $style = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( $text == '' ) {
+ $text = $this->linkText( $title );
+ }
+ $attribs = Sanitizer::mergeAttributes(
+ Sanitizer::decodeTagAttributes( $aprops ),
+ Sanitizer::decodeTagAttributes( $style )
+ );
+ $query = wfCgiToArray( $query );
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+
+ $ret = $this->link( $title, "$prefix$text$inside", $attribs, $query,
+ array( 'known', 'noclasses' ) ) . $trail;
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * Make a red link to the edit page of a given title.
+ *
+ * @param $nt Title object of the target page
+ * @param $text String: Link text
+ * @param $query String: Optional query part
+ * @param $trail String: Optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeBrokenLinkObj( $title, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ wfProfileIn( __METHOD__ );
+
+ list( $inside, $trail ) = Linker::splitTrail( $trail );
+ if( $text === '' ) {
+ $text = $this->linkText( $title );
+ }
+ $nt = $this->normaliseSpecialPage( $title );
+
+ $ret = $this->link( $title, "$prefix$text$inside", array(),
+ wfCgiToArray( $query ), 'broken' ) . $trail;
+
+ wfProfileOut( __METHOD__ );
+ return $ret;
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * 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
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeStubLinkObj( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ wfDeprecated( __METHOD__ );
+ return $this->makeColouredLinkObj( $nt, 'stub', $text, $query, $trail, $prefix );
+ }
+
+ /**
+ * @deprecated Use link()
+ *
+ * Make a coloured link.
+ *
+ * @param $nt Title object of the target page
+ * @param $colour Integer: colour of the link
+ * @param $text String: link text
+ * @param $query String: optional query part
+ * @param $trail String: optional trail. Alphabetic characters at the start of this string will
+ * be included in the link text. Other characters will be appended after
+ * the end of the link.
+ */
+ function makeColouredLinkObj( $nt, $colour, $text = '', $query = '', $trail = '', $prefix = '' ) {
+ if($colour != ''){
+ $style = $this->getInternalLinkAttributesObj( $nt, $text, $colour );
+ } else $style = '';
+ return $this->makeKnownLinkObj( $nt, $text, $query, $trail, $prefix, '', $style );
+ }
+
+ /** Obsolete alias */
+ function makeImage( $url, $alt = '' ) {
+ wfDeprecated( __METHOD__ );
+ return $this->makeExternalImage( $url, $alt );
+ }
+
+ /**
+ * Creates the HTML source for images
+ * @deprecated use makeImageLink2
+ *
+ * @param object $title
+ * @param string $label label text
+ * @param string $alt alt text
+ * @param string $align horizontal alignment: none, left, center, right)
+ * @param array $handlerParams Parameters to be passed to the media handler
+ * @param boolean $framed shows image in original size in a frame
+ * @param boolean $thumb shows image as thumbnail in a frame
+ * @param string $manualthumb image name for the manual thumbnail
+ * @param string $valign vertical alignment: baseline, sub, super, top, text-top, middle, bottom, text-bottom
+ * @param string $time, timestamp of the file, set as false for current
+ * @return string
+ */
+ function makeImageLinkObj( $title, $label, $alt, $align = '', $handlerParams = array(), $framed = false,
+ $thumb = false, $manualthumb = '', $valign = '', $time = false )
+ {
+ $frameParams = array( 'alt' => $alt, 'caption' => $label );
+ if ( $align ) {
+ $frameParams['align'] = $align;
+ }
+ if ( $framed ) {
+ $frameParams['framed'] = true;
+ }
+ if ( $thumb ) {
+ $frameParams['thumbnail'] = true;
+ }
+ if ( $manualthumb ) {
+ $frameParams['manualthumb'] = $manualthumb;
+ }
+ if ( $valign ) {
+ $frameParams['valign'] = $valign;
+ }
+ $file = wfFindFile( $title, array( 'time' => $time ) );
+ return $this->makeImageLink2( $title, $file, $frameParams, $handlerParams, $time );
+ }
+
+ /** @deprecated use Linker::makeMediaLinkObj() */
+ function makeMediaLink( $name, $unused = '', $text = '', $time = false ) {
+ $nt = Title::makeTitleSafe( NS_FILE, $name );
+ return $this->makeMediaLinkObj( $nt, $text, $time );
+ }
+
+ /**
+ * Used to generate section edit links that point to "other" pages
+ * (sections that are really part of included pages).
+ *
+ * @deprecated use Linker::doEditSectionLink()
+ * @param $title Title string.
+ * @param $section Integer: section number.
+ */
+ public function editSectionLinkForOther( $title, $section ) {
+ wfDeprecated( __METHOD__ );
+ $title = Title::newFromText( $title );
+ return $this->doEditSectionLink( $title, $section );
+ }
+
+ /**
+ * @deprecated use Linker::doEditSectionLink()
+ * @param $nt Title object.
+ * @param $section Integer: section number.
+ * @param $hint Link String: title, or default if omitted or empty
+ */
+ public function editSectionLink( Title $nt, $section, $hint = '' ) {
+ wfDeprecated( __METHOD__ );
+ if( $hint === '' ) {
+ # No way to pass an actual empty $hint here! The new interface al-
+ # lows this, so we have to do this for compatibility.
+ $hint = null;
+ }
+ return $this->doEditSectionLink( $nt, $section, $hint );
+ }
+
+ /**
+ * Returns the attributes for the tooltip and access key
+ */
+ public function tooltipAndAccesskeyAttribs( $name ) {
+ global $wgEnableTooltipsAndAccesskeys;
+ if ( !$wgEnableTooltipsAndAccesskeys )
+ return array();
+ # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
+ # no attribute" instead of "output '' as value for attribute", this
+ # would be three lines.
+ $attribs = array(
+ 'title' => $this->titleAttrib( $name, 'withaccess' ),
+ 'accesskey' => $this->accesskey( $name )
+ );
+ if ( $attribs['title'] === false ) {
+ unset( $attribs['title'] );
+ }
+ if ( $attribs['accesskey'] === false ) {
+ unset( $attribs['accesskey'] );
+ }
+ return $attribs;
+ }
+ /**
+ * @deprecated Returns raw bits of HTML, use titleAttrib() and accesskey()
+ */
+ public function tooltipAndAccesskey( $name ) {
+ return Xml::expandAttributes( $this->tooltipAndAccesskeyAttribs( $name ) );
+ }
+
+
+ /** @deprecated Returns raw bits of HTML, use titleAttrib() */
+ public function tooltip( $name, $options = null ) {
+ global $wgEnableTooltipsAndAccesskeys;
+ if ( !$wgEnableTooltipsAndAccesskeys )
+ return '';
+ # FIXME: If Sanitizer::expandAttributes() treated "false" as "output
+ # no attribute" instead of "output '' as value for attribute", this
+ # would be two lines.
+ $tooltip = $this->titleAttrib( $name, $options );
+ if ( $tooltip === false ) {
+ return '';
+ }
+ return Xml::expandAttributes( array(
+ 'title' => $this->titleAttrib( $name, $options )
+ ) );
+ }
}
diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php
index caacb49c..ef3374d9 100644
--- a/includes/LinksUpdate.php
+++ b/includes/LinksUpdate.php
@@ -673,6 +673,13 @@ class LinksUpdate {
function getTitle() {
return $this->mTitle;
}
+
+ /**
+ * Return the list of images used as generated by the parser
+ */
+ public function getImages() {
+ return $this->mImages;
+ }
/**
* Invalidate any necessary link lists related to page property changes
diff --git a/includes/LocalisationCache.php b/includes/LocalisationCache.php
new file mode 100644
index 00000000..12925b68
--- /dev/null
+++ b/includes/LocalisationCache.php
@@ -0,0 +1,999 @@
+<?php
+
+define( 'MW_LC_VERSION', 1 );
+
+/**
+ * Class for caching the contents of localisation files, Messages*.php
+ * and *.i18n.php.
+ *
+ * An instance of this class is available using Language::getLocalisationCache().
+ *
+ * The values retrieved from here are merged, containing items from extension
+ * files, core messages files and the language fallback sequence (e.g. zh-cn ->
+ * zh-hans -> en ). Some common errors are corrected, for example namespace
+ * names with spaces instead of underscores, but heavyweight processing, such
+ * as grammatical transformation, is done by the caller.
+ */
+class LocalisationCache {
+ /** Configuration associative array */
+ var $conf;
+
+ /**
+ * True if recaching should only be done on an explicit call to recache().
+ * Setting this reduces the overhead of cache freshness checking, which
+ * requires doing a stat() for every extension i18n file.
+ */
+ var $manualRecache = false;
+
+ /**
+ * True to treat all files as expired until they are regenerated by this object.
+ */
+ var $forceRecache = false;
+
+ /**
+ * The cache data. 3-d array, where the first key is the language code,
+ * the second key is the item key e.g. 'messages', and the third key is
+ * an item specific subkey index. Some items are not arrays and so for those
+ * items, there are no subkeys.
+ */
+ var $data = array();
+
+ /**
+ * The persistent store object. An instance of LCStore.
+ */
+ var $store;
+
+ /**
+ * A 2-d associative array, code/key, where presence indicates that the item
+ * is loaded. Value arbitrary.
+ *
+ * For split items, if set, this indicates that all of the subitems have been
+ * loaded.
+ */
+ var $loadedItems = array();
+
+ /**
+ * A 3-d associative array, code/key/subkey, where presence indicates that
+ * the subitem is loaded. Only used for the split items, i.e. messages.
+ */
+ var $loadedSubitems = array();
+
+ /**
+ * An array where presence of a key indicates that that language has been
+ * initialised. Initialisation includes checking for cache expiry and doing
+ * any necessary updates.
+ */
+ var $initialisedLangs = array();
+
+ /**
+ * An array mapping non-existent pseudo-languages to fallback languages. This
+ * is filled by initShallowFallback() when data is requested from a language
+ * that lacks a Messages*.php file.
+ */
+ var $shallowFallbacks = array();
+
+ /**
+ * An array where the keys are codes that have been recached by this instance.
+ */
+ var $recachedLangs = array();
+
+ /**
+ * Data added by extensions using the deprecated $wgMessageCache->addMessages()
+ * interface.
+ */
+ var $legacyData = array();
+
+ /**
+ * All item keys
+ */
+ static public $allKeys = array(
+ 'fallback', 'namespaceNames', 'mathNames', 'bookstoreList',
+ 'magicWords', 'messages', 'rtl', 'capitalizeAllNouns', 'digitTransformTable',
+ 'separatorTransformTable', 'fallback8bitEncoding', 'linkPrefixExtension',
+ 'defaultUserOptionOverrides', 'linkTrail', 'namespaceAliases',
+ 'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
+ 'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
+ 'imageFiles', 'preloadedMessages',
+ );
+
+ /**
+ * Keys for items which consist of associative arrays, which may be merged
+ * by a fallback sequence.
+ */
+ static public $mergeableMapKeys = array( 'messages', 'namespaceNames', 'mathNames',
+ 'dateFormats', 'defaultUserOptionOverrides', 'magicWords', 'imageFiles',
+ 'preloadedMessages',
+ );
+
+ /**
+ * Keys for items which are a numbered array.
+ */
+ static public $mergeableListKeys = array( 'extraUserToggles' );
+
+ /**
+ * Keys for items which contain an array of arrays of equivalent aliases
+ * for each subitem. The aliases may be merged by a fallback sequence.
+ */
+ static public $mergeableAliasListKeys = array( 'specialPageAliases' );
+
+ /**
+ * Keys for items which contain an associative array, and may be merged if
+ * the primary value contains the special array key "inherit". That array
+ * key is removed after the first merge.
+ */
+ static public $optionalMergeKeys = array( 'bookstoreList' );
+
+ /**
+ * Keys for items where the subitems are stored in the backend separately.
+ */
+ static public $splitKeys = array( 'messages' );
+
+ /**
+ * Keys which are loaded automatically by initLanguage()
+ */
+ static public $preloadedKeys = array( 'dateFormats', 'namespaceNames',
+ 'defaultUserOptionOverrides' );
+
+ /**
+ * Constructor.
+ * For constructor parameters, see the documentation in DefaultSettings.php
+ * for $wgLocalisationCacheConf.
+ */
+ function __construct( $conf ) {
+ global $wgCacheDirectory;
+
+ $this->conf = $conf;
+ $storeConf = array();
+ if ( !empty( $conf['storeClass'] ) ) {
+ $storeClass = $conf['storeClass'];
+ } else {
+ switch ( $conf['store'] ) {
+ case 'files':
+ case 'file':
+ $storeClass = 'LCStore_CDB';
+ break;
+ case 'db':
+ $storeClass = 'LCStore_DB';
+ break;
+ case 'detect':
+ $storeClass = $wgCacheDirectory ? 'LCStore_CDB' : 'LCStore_DB';
+ break;
+ default:
+ throw new MWException(
+ 'Please set $wgLocalisationCacheConf[\'store\'] to something sensible.' );
+ }
+ }
+
+ wfDebug( get_class( $this ) . ": using store $storeClass\n" );
+ if ( !empty( $conf['storeDirectory'] ) ) {
+ $storeConf['directory'] = $conf['storeDirectory'];
+ }
+
+ $this->store = new $storeClass( $storeConf );
+ foreach ( array( 'manualRecache', 'forceRecache' ) as $var ) {
+ if ( isset( $conf[$var] ) ) {
+ $this->$var = $conf[$var];
+ }
+ }
+ }
+
+ /**
+ * Returns true if the given key is mergeable, that is, if it is an associative
+ * array which can be merged through a fallback sequence.
+ */
+ public function isMergeableKey( $key ) {
+ if ( !isset( $this->mergeableKeys ) ) {
+ $this->mergeableKeys = array_flip( array_merge(
+ self::$mergeableMapKeys,
+ self::$mergeableListKeys,
+ self::$mergeableAliasListKeys,
+ self::$optionalMergeKeys
+ ) );
+ }
+ return isset( $this->mergeableKeys[$key] );
+ }
+
+ /**
+ * Get a cache item.
+ *
+ * Warning: this may be slow for split items (messages), since it will
+ * need to fetch all of the subitems from the cache individually.
+ */
+ public function getItem( $code, $key ) {
+ if ( !isset( $this->loadedItems[$code][$key] ) ) {
+ wfProfileIn( __METHOD__.'-load' );
+ $this->loadItem( $code, $key );
+ wfProfileOut( __METHOD__.'-load' );
+ }
+ if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
+ return $this->shallowFallbacks[$code];
+ }
+ return $this->data[$code][$key];
+ }
+
+ /**
+ * Get a subitem, for instance a single message for a given language.
+ */
+ public function getSubitem( $code, $key, $subkey ) {
+ if ( isset( $this->legacyData[$code][$key][$subkey] ) ) {
+ return $this->legacyData[$code][$key][$subkey];
+ }
+ if ( !isset( $this->loadedSubitems[$code][$key][$subkey] )
+ && !isset( $this->loadedItems[$code][$key] ) )
+ {
+ wfProfileIn( __METHOD__.'-load' );
+ $this->loadSubitem( $code, $key, $subkey );
+ wfProfileOut( __METHOD__.'-load' );
+ }
+ if ( isset( $this->data[$code][$key][$subkey] ) ) {
+ return $this->data[$code][$key][$subkey];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the list of subitem keys for a given item.
+ *
+ * This is faster than array_keys($lc->getItem(...)) for the items listed in
+ * self::$splitKeys.
+ *
+ * Will return null if the item is not found, or false if the item is not an
+ * array.
+ */
+ public function getSubitemList( $code, $key ) {
+ if ( in_array( $key, self::$splitKeys ) ) {
+ return $this->getSubitem( $code, 'list', $key );
+ } else {
+ $item = $this->getItem( $code, $key );
+ if ( is_array( $item ) ) {
+ return array_keys( $item );
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Load an item into the cache.
+ */
+ protected function loadItem( $code, $key ) {
+ if ( !isset( $this->initialisedLangs[$code] ) ) {
+ $this->initLanguage( $code );
+ }
+ // Check to see if initLanguage() loaded it for us
+ if ( isset( $this->loadedItems[$code][$key] ) ) {
+ return;
+ }
+ if ( isset( $this->shallowFallbacks[$code] ) ) {
+ $this->loadItem( $this->shallowFallbacks[$code], $key );
+ return;
+ }
+ if ( in_array( $key, self::$splitKeys ) ) {
+ $subkeyList = $this->getSubitem( $code, 'list', $key );
+ foreach ( $subkeyList as $subkey ) {
+ if ( isset( $this->data[$code][$key][$subkey] ) ) {
+ continue;
+ }
+ $this->data[$code][$key][$subkey] = $this->getSubitem( $code, $key, $subkey );
+ }
+ } else {
+ $this->data[$code][$key] = $this->store->get( $code, $key );
+ }
+ $this->loadedItems[$code][$key] = true;
+ }
+
+ /**
+ * Load a subitem into the cache
+ */
+ protected function loadSubitem( $code, $key, $subkey ) {
+ if ( !in_array( $key, self::$splitKeys ) ) {
+ $this->loadItem( $code, $key );
+ return;
+ }
+ if ( !isset( $this->initialisedLangs[$code] ) ) {
+ $this->initLanguage( $code );
+ }
+ // Check to see if initLanguage() loaded it for us
+ if ( isset( $this->loadedItems[$code][$key] )
+ || isset( $this->loadedSubitems[$code][$key][$subkey] ) )
+ {
+ return;
+ }
+ if ( isset( $this->shallowFallbacks[$code] ) ) {
+ $this->loadSubitem( $this->shallowFallbacks[$code], $key, $subkey );
+ return;
+ }
+ $value = $this->store->get( $code, "$key:$subkey" );
+ $this->data[$code][$key][$subkey] = $value;
+ $this->loadedSubitems[$code][$key][$subkey] = true;
+ }
+
+ /**
+ * Returns true if the cache identified by $code is missing or expired.
+ */
+ public function isExpired( $code ) {
+ if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
+ wfDebug( __METHOD__."($code): forced reload\n" );
+ return true;
+ }
+
+ $deps = $this->store->get( $code, 'deps' );
+ if ( $deps === null ) {
+ wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
+ return true;
+ }
+ foreach ( $deps as $dep ) {
+ // Because we're unserializing stuff from cache, we
+ // could receive objects of classes that don't exist
+ // anymore (e.g. uninstalled extensions)
+ // When this happens, always expire the cache
+ if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
+ wfDebug( __METHOD__."($code): cache for $code expired due to " .
+ get_class( $dep ) . "\n" );
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Initialise a language in this object. Rebuild the cache if necessary.
+ */
+ protected function initLanguage( $code ) {
+ if ( isset( $this->initialisedLangs[$code] ) ) {
+ return;
+ }
+ $this->initialisedLangs[$code] = true;
+
+ # Recache the data if necessary
+ if ( !$this->manualRecache && $this->isExpired( $code ) ) {
+ if ( file_exists( Language::getMessagesFileName( $code ) ) ) {
+ $this->recache( $code );
+ } elseif ( $code === 'en' ) {
+ throw new MWException( 'MessagesEn.php is missing.' );
+ } else {
+ $this->initShallowFallback( $code, 'en' );
+ }
+ return;
+ }
+
+ # Preload some stuff
+ $preload = $this->getItem( $code, 'preload' );
+ if ( $preload === null ) {
+ if ( $this->manualRecache ) {
+ // No Messages*.php file. Do shallow fallback to en.
+ if ( $code === 'en' ) {
+ throw new MWException( 'No localisation cache found for English. ' .
+ 'Please run maintenance/rebuildLocalisationCache.php.' );
+ }
+ $this->initShallowFallback( $code, 'en' );
+ return;
+ } else {
+ throw new MWException( 'Invalid or missing localisation cache.' );
+ }
+ }
+ $this->data[$code] = $preload;
+ foreach ( $preload as $key => $item ) {
+ if ( in_array( $key, self::$splitKeys ) ) {
+ foreach ( $item as $subkey => $subitem ) {
+ $this->loadedSubitems[$code][$key][$subkey] = true;
+ }
+ } else {
+ $this->loadedItems[$code][$key] = true;
+ }
+ }
+ }
+
+ /**
+ * Create a fallback from one language to another, without creating a
+ * complete persistent cache.
+ */
+ public function initShallowFallback( $primaryCode, $fallbackCode ) {
+ $this->data[$primaryCode] =& $this->data[$fallbackCode];
+ $this->loadedItems[$primaryCode] =& $this->loadedItems[$fallbackCode];
+ $this->loadedSubitems[$primaryCode] =& $this->loadedSubitems[$fallbackCode];
+ $this->shallowFallbacks[$primaryCode] = $fallbackCode;
+ }
+
+ /**
+ * Read a PHP file containing localisation data.
+ */
+ protected function readPHPFile( $_fileName, $_fileType ) {
+ // Disable APC caching
+ $_apcEnabled = ini_set( 'apc.cache_by_default', '0' );
+ include( $_fileName );
+ ini_set( 'apc.cache_by_default', $_apcEnabled );
+
+ if ( $_fileType == 'core' || $_fileType == 'extension' ) {
+ $data = compact( self::$allKeys );
+ } elseif ( $_fileType == 'aliases' ) {
+ $data = compact( 'aliases' );
+ } else {
+ throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
+ }
+ return $data;
+ }
+
+ /**
+ * Merge two localisation values, a primary and a fallback, overwriting the
+ * primary value in place.
+ */
+ protected function mergeItem( $key, &$value, $fallbackValue ) {
+ if ( !is_null( $value ) ) {
+ if ( !is_null( $fallbackValue ) ) {
+ if ( in_array( $key, self::$mergeableMapKeys ) ) {
+ $value = $value + $fallbackValue;
+ } elseif ( in_array( $key, self::$mergeableListKeys ) ) {
+ $value = array_unique( array_merge( $fallbackValue, $value ) );
+ } elseif ( in_array( $key, self::$mergeableAliasListKeys ) ) {
+ $value = array_merge_recursive( $value, $fallbackValue );
+ } elseif ( in_array( $key, self::$optionalMergeKeys ) ) {
+ if ( !empty( $value['inherit'] ) ) {
+ $value = array_merge( $fallbackValue, $value );
+ }
+ if ( isset( $value['inherit'] ) ) {
+ unset( $value['inherit'] );
+ }
+ }
+ }
+ } else {
+ $value = $fallbackValue;
+ }
+ }
+
+ /**
+ * Given an array mapping language code to localisation value, such as is
+ * found in extension *.i18n.php files, iterate through a fallback sequence
+ * to merge the given data with an existing primary value.
+ *
+ * Returns true if any data from the extension array was used, false
+ * otherwise.
+ */
+ protected function mergeExtensionItem( $codeSequence, $key, &$value, $fallbackValue ) {
+ $used = false;
+ foreach ( $codeSequence as $code ) {
+ if ( isset( $fallbackValue[$code] ) ) {
+ $this->mergeItem( $key, $value, $fallbackValue[$code] );
+ $used = true;
+ }
+ }
+ return $used;
+ }
+
+ /**
+ * Load localisation data for a given language for both core and extensions
+ * and save it to the persistent cache store and the process cache
+ */
+ public function recache( $code ) {
+ static $recursionGuard = array();
+ global $wgExtensionMessagesFiles, $wgExtensionAliasesFiles;
+ wfProfileIn( __METHOD__ );
+
+ if ( !$code ) {
+ throw new MWException( "Invalid language code requested" );
+ }
+ $this->recachedLangs[$code] = true;
+
+ # Initial values
+ $initialData = array_combine(
+ self::$allKeys,
+ array_fill( 0, count( self::$allKeys ), null ) );
+ $coreData = $initialData;
+ $deps = array();
+
+ # Load the primary localisation from the source file
+ $fileName = Language::getMessagesFileName( $code );
+ if ( !file_exists( $fileName ) ) {
+ wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
+ $coreData['fallback'] = 'en';
+ } else {
+ $deps[] = new FileDependency( $fileName );
+ $data = $this->readPHPFile( $fileName, 'core' );
+ wfDebug( __METHOD__.": got localisation for $code from source\n" );
+
+ # Merge primary localisation
+ foreach ( $data as $key => $value ) {
+ $this->mergeItem( $key, $coreData[$key], $value );
+ }
+ }
+
+ # Fill in the fallback if it's not there already
+ if ( is_null( $coreData['fallback'] ) ) {
+ $coreData['fallback'] = $code === 'en' ? false : 'en';
+ }
+
+ if ( $coreData['fallback'] !== false ) {
+ # Guard against circular references
+ if ( isset( $recursionGuard[$code] ) ) {
+ throw new MWException( "Error: Circular fallback reference in language code $code" );
+ }
+ $recursionGuard[$code] = true;
+
+ # Load the fallback localisation item by item and merge it
+ $deps = array_merge( $deps, $this->getItem( $coreData['fallback'], 'deps' ) );
+ foreach ( self::$allKeys as $key ) {
+ if ( is_null( $coreData[$key] ) || $this->isMergeableKey( $key ) ) {
+ $fallbackValue = $this->getItem( $coreData['fallback'], $key );
+ $this->mergeItem( $key, $coreData[$key], $fallbackValue );
+ }
+ }
+ $fallbackSequence = $this->getItem( $coreData['fallback'], 'fallbackSequence' );
+ array_unshift( $fallbackSequence, $coreData['fallback'] );
+ $coreData['fallbackSequence'] = $fallbackSequence;
+ unset( $recursionGuard[$code] );
+ } else {
+ $coreData['fallbackSequence'] = array();
+ }
+ $codeSequence = array_merge( array( $code ), $coreData['fallbackSequence'] );
+
+ # Load the extension localisations
+ # This is done after the core because we know the fallback sequence now.
+ # But it has a higher precedence for merging so that we can support things
+ # like site-specific message overrides.
+ $allData = $initialData;
+ foreach ( $wgExtensionMessagesFiles as $fileName ) {
+ $data = $this->readPHPFile( $fileName, 'extension' );
+ $used = false;
+ foreach ( $data as $key => $item ) {
+ if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
+ $used = true;
+ }
+ }
+ if ( $used ) {
+ $deps[] = new FileDependency( $fileName );
+ }
+ }
+
+ # Load deprecated $wgExtensionAliasesFiles
+ foreach ( $wgExtensionAliasesFiles as $fileName ) {
+ $data = $this->readPHPFile( $fileName, 'aliases' );
+ if ( !isset( $data['aliases'] ) ) {
+ continue;
+ }
+ $used = $this->mergeExtensionItem( $codeSequence, 'specialPageAliases',
+ $allData['specialPageAliases'], $data['aliases'] );
+ if ( $used ) {
+ $deps[] = new FileDependency( $fileName );
+ }
+ }
+
+ # Merge core data into extension data
+ foreach ( $coreData as $key => $item ) {
+ $this->mergeItem( $key, $allData[$key], $item );
+ }
+
+ # Add cache dependencies for any referenced globals
+ $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' );
+ $deps['wgExtensionAliasesFiles'] = new GlobalDependency( 'wgExtensionAliasesFiles' );
+ $deps['version'] = new ConstantDependency( 'MW_LC_VERSION' );
+
+ # Add dependencies to the cache entry
+ $allData['deps'] = $deps;
+
+ # Replace spaces with underscores in namespace names
+ $allData['namespaceNames'] = str_replace( ' ', '_', $allData['namespaceNames'] );
+
+ # And do the same for special page aliases. $page is an array.
+ foreach ( $allData['specialPageAliases'] as &$page ) {
+ $page = str_replace( ' ', '_', $page );
+ }
+ # Decouple the reference to prevent accidental damage
+ unset($page);
+
+ # Fix broken defaultUserOptionOverrides
+ if ( !is_array( $allData['defaultUserOptionOverrides'] ) ) {
+ $allData['defaultUserOptionOverrides'] = array();
+ }
+
+ # Set the list keys
+ $allData['list'] = array();
+ foreach ( self::$splitKeys as $key ) {
+ $allData['list'][$key] = array_keys( $allData[$key] );
+ }
+
+ # Run hooks
+ wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
+
+ if ( is_null( $allData['defaultUserOptionOverrides'] ) ) {
+ throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
+ 'Check that your languages/messages/MessagesEn.php file is intact.' );
+ }
+
+ # Set the preload key
+ $allData['preload'] = $this->buildPreload( $allData );
+
+ # Save to the process cache and register the items loaded
+ $this->data[$code] = $allData;
+ foreach ( $allData as $key => $item ) {
+ $this->loadedItems[$code][$key] = true;
+ }
+
+ # Save to the persistent cache
+ $this->store->startWrite( $code );
+ foreach ( $allData as $key => $value ) {
+ if ( in_array( $key, self::$splitKeys ) ) {
+ foreach ( $value as $subkey => $subvalue ) {
+ $this->store->set( "$key:$subkey", $subvalue );
+ }
+ } else {
+ $this->store->set( $key, $value );
+ }
+ }
+ $this->store->finishWrite();
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Build the preload item from the given pre-cache data.
+ *
+ * The preload item will be loaded automatically, improving performance
+ * for the commonly-requested items it contains.
+ */
+ protected function buildPreload( $data ) {
+ $preload = array( 'messages' => array() );
+ foreach ( self::$preloadedKeys as $key ) {
+ $preload[$key] = $data[$key];
+ }
+ foreach ( $data['preloadedMessages'] as $subkey ) {
+ if ( isset( $data['messages'][$subkey] ) ) {
+ $subitem = $data['messages'][$subkey];
+ } else {
+ $subitem = null;
+ }
+ $preload['messages'][$subkey] = $subitem;
+ }
+ return $preload;
+ }
+
+ /**
+ * Unload the data for a given language from the object cache.
+ * Reduces memory usage.
+ */
+ public function unload( $code ) {
+ unset( $this->data[$code] );
+ unset( $this->loadedItems[$code] );
+ unset( $this->loadedSubitems[$code] );
+ unset( $this->initialisedLangs[$code] );
+ // We don't unload legacyData because there's no way to get it back
+ // again, it's not really a cache
+ foreach ( $this->shallowFallbacks as $shallowCode => $fbCode ) {
+ if ( $fbCode === $code ) {
+ $this->unload( $shallowCode );
+ }
+ }
+ }
+
+ /**
+ * Unload all data
+ */
+ public function unloadAll() {
+ foreach ( $this->initialisedLangs as $lang => $unused ) {
+ $this->unload( $lang );
+ }
+ }
+
+ /**
+ * Add messages to the cache, from an extension that has not yet been
+ * migrated to $wgExtensionMessages or the LocalisationCacheRecache hook.
+ * Called by deprecated function $wgMessageCache->addMessages().
+ */
+ public function addLegacyMessages( $messages ) {
+ foreach ( $messages as $lang => $langMessages ) {
+ if ( isset( $this->legacyData[$lang]['messages'] ) ) {
+ $this->legacyData[$lang]['messages'] =
+ $langMessages + $this->legacyData[$lang]['messages'];
+ } else {
+ $this->legacyData[$lang]['messages'] = $langMessages;
+ }
+ }
+ }
+
+ /**
+ * Disable the storage backend
+ */
+ public function disableBackend() {
+ $this->store = new LCStore_Null;
+ $this->manualRecache = false;
+ }
+}
+
+/**
+ * Interface for the persistence layer of LocalisationCache.
+ *
+ * The persistence layer is two-level hierarchical cache. The first level
+ * is the language, the second level is the item or subitem.
+ *
+ * Since the data for a whole language is rebuilt in one operation, it needs
+ * to have a fast and atomic method for deleting or replacing all of the
+ * current data for a given language. The interface reflects this bulk update
+ * operation. Callers writing to the cache must first call startWrite(), then
+ * will call set() a couple of thousand times, then will call finishWrite()
+ * to commit the operation. When finishWrite() is called, the cache is
+ * expected to delete all data previously stored for that language.
+ *
+ * The values stored are PHP variables suitable for serialize(). Implementations
+ * of LCStore are responsible for serializing and unserializing.
+ */
+interface LCStore {
+ /**
+ * Get a value.
+ * @param $code Language code
+ * @param $key Cache key
+ */
+ public function get( $code, $key );
+
+ /**
+ * Start a write transaction.
+ * @param $code Language code
+ */
+ public function startWrite( $code );
+
+ /**
+ * Finish a write transaction.
+ */
+ public function finishWrite();
+
+ /**
+ * Set a key to a given value. startWrite() must be called before this
+ * is called, and finishWrite() must be called afterwards.
+ */
+ public function set( $key, $value );
+
+}
+
+/**
+ * LCStore implementation which uses the standard DB functions to store data.
+ * This will work on any MediaWiki installation.
+ */
+class LCStore_DB implements LCStore {
+ var $currentLang;
+ var $writesDone = false;
+ var $dbw, $batch;
+ var $readOnly = false;
+
+ public function get( $code, $key ) {
+ if ( $this->writesDone ) {
+ $db = wfGetDB( DB_MASTER );
+ } else {
+ $db = wfGetDB( DB_SLAVE );
+ }
+ $row = $db->selectRow( 'l10n_cache', array( 'lc_value' ),
+ array( 'lc_lang' => $code, 'lc_key' => $key ), __METHOD__ );
+ if ( $row ) {
+ return unserialize( $row->lc_value );
+ } else {
+ return null;
+ }
+ }
+
+ public function startWrite( $code ) {
+ if ( $this->readOnly ) {
+ return;
+ }
+ if ( !$code ) {
+ throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+ }
+ $this->dbw = wfGetDB( DB_MASTER );
+ try {
+ $this->dbw->begin();
+ $this->dbw->delete( 'l10n_cache', array( 'lc_lang' => $code ), __METHOD__ );
+ } catch ( DBQueryError $e ) {
+ if ( $this->dbw->wasReadOnlyError() ) {
+ $this->readOnly = true;
+ $this->dbw->rollback();
+ $this->dbw->ignoreErrors( false );
+ return;
+ } else {
+ throw $e;
+ }
+ }
+ $this->currentLang = $code;
+ $this->batch = array();
+ }
+
+ public function finishWrite() {
+ if ( $this->readOnly ) {
+ return;
+ }
+ if ( $this->batch ) {
+ $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
+ }
+ $this->dbw->commit();
+ $this->currentLang = null;
+ $this->dbw = null;
+ $this->batch = array();
+ $this->writesDone = true;
+ }
+
+ public function set( $key, $value ) {
+ if ( $this->readOnly ) {
+ return;
+ }
+ if ( is_null( $this->currentLang ) ) {
+ throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+ }
+ $this->batch[] = array(
+ 'lc_lang' => $this->currentLang,
+ 'lc_key' => $key,
+ 'lc_value' => serialize( $value ) );
+ if ( count( $this->batch ) >= 100 ) {
+ $this->dbw->insert( 'l10n_cache', $this->batch, __METHOD__ );
+ $this->batch = array();
+ }
+ }
+}
+
+/**
+ * LCStore implementation which stores data as a collection of CDB files in the
+ * directory given by $wgCacheDirectory. If $wgCacheDirectory is not set, this
+ * will throw an exception.
+ *
+ * Profiling indicates that on Linux, this implementation outperforms MySQL if
+ * the directory is on a local filesystem and there is ample kernel cache
+ * space. The performance advantage is greater when the DBA extension is
+ * available than it is with the PHP port.
+ *
+ * See Cdb.php and http://cr.yp.to/cdb.html
+ */
+class LCStore_CDB implements LCStore {
+ var $readers, $writer, $currentLang, $directory;
+
+ function __construct( $conf = array() ) {
+ global $wgCacheDirectory;
+ if ( isset( $conf['directory'] ) ) {
+ $this->directory = $conf['directory'];
+ } else {
+ $this->directory = $wgCacheDirectory;
+ }
+ }
+
+ public function get( $code, $key ) {
+ if ( !isset( $this->readers[$code] ) ) {
+ $fileName = $this->getFileName( $code );
+ if ( !file_exists( $fileName ) ) {
+ $this->readers[$code] = false;
+ } else {
+ $this->readers[$code] = CdbReader::open( $fileName );
+ }
+ }
+ if ( !$this->readers[$code] ) {
+ return null;
+ } else {
+ $value = $this->readers[$code]->get( $key );
+ if ( $value === false ) {
+ return null;
+ }
+ return unserialize( $value );
+ }
+ }
+
+ public function startWrite( $code ) {
+ if ( !file_exists( $this->directory ) ) {
+ if ( !wfMkdirParents( $this->directory ) ) {
+ throw new MWException( "Unable to create the localisation store " .
+ "directory \"{$this->directory}\"" );
+ }
+ }
+ // Close reader to stop permission errors on write
+ if( !empty($this->readers[$code]) ) {
+ $this->readers[$code]->close();
+ }
+ $this->writer = CdbWriter::open( $this->getFileName( $code ) );
+ $this->currentLang = $code;
+ }
+
+ public function finishWrite() {
+ // Close the writer
+ $this->writer->close();
+ $this->writer = null;
+ unset( $this->readers[$this->currentLang] );
+ $this->currentLang = null;
+ }
+
+ public function set( $key, $value ) {
+ if ( is_null( $this->writer ) ) {
+ throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+ }
+ $this->writer->set( $key, serialize( $value ) );
+ }
+
+ protected function getFileName( $code ) {
+ if ( !$code || strpos( $code, '/' ) !== false ) {
+ throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+ }
+ return "{$this->directory}/l10n_cache-$code.cdb";
+ }
+}
+
+/**
+ * Null store backend, used to avoid DB errors during install
+ */
+class LCStore_Null implements LCStore {
+ public function get( $code, $key ) {
+ return null;
+ }
+
+ public function startWrite( $code ) {}
+ public function finishWrite() {}
+ public function set( $key, $value ) {}
+}
+
+/**
+ * A localisation cache optimised for loading large amounts of data for many
+ * languages. Used by rebuildLocalisationCache.php.
+ */
+class LocalisationCache_BulkLoad extends LocalisationCache {
+ /**
+ * A cache of the contents of data files.
+ * Core files are serialized to avoid using ~1GB of RAM during a recache.
+ */
+ var $fileCache = array();
+
+ /**
+ * Most recently used languages. Uses the linked-list aspect of PHP hashtables
+ * to keep the most recently used language codes at the end of the array, and
+ * the language codes that are ready to be deleted at the beginning.
+ */
+ var $mruLangs = array();
+
+ /**
+ * Maximum number of languages that may be loaded into $this->data
+ */
+ var $maxLoadedLangs = 10;
+
+ protected function readPHPFile( $fileName, $fileType ) {
+ $serialize = $fileType === 'core';
+ if ( !isset( $this->fileCache[$fileName][$fileType] ) ) {
+ $data = parent::readPHPFile( $fileName, $fileType );
+ if ( $serialize ) {
+ $encData = serialize( $data );
+ } else {
+ $encData = $data;
+ }
+ $this->fileCache[$fileName][$fileType] = $encData;
+ return $data;
+ } elseif ( $serialize ) {
+ return unserialize( $this->fileCache[$fileName][$fileType] );
+ } else {
+ return $this->fileCache[$fileName][$fileType];
+ }
+ }
+
+ public function getItem( $code, $key ) {
+ unset( $this->mruLangs[$code] );
+ $this->mruLangs[$code] = true;
+ return parent::getItem( $code, $key );
+ }
+
+ public function getSubitem( $code, $key, $subkey ) {
+ unset( $this->mruLangs[$code] );
+ $this->mruLangs[$code] = true;
+ return parent::getSubitem( $code, $key, $subkey );
+ }
+
+ public function recache( $code ) {
+ parent::recache( $code );
+ unset( $this->mruLangs[$code] );
+ $this->mruLangs[$code] = true;
+ $this->trimCache();
+ }
+
+ public function unload( $code ) {
+ unset( $this->mruLangs[$code] );
+ parent::unload( $code );
+ }
+
+ /**
+ * Unload cached languages until there are less than $this->maxLoadedLangs
+ */
+ protected function trimCache() {
+ while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
+ reset( $this->mruLangs );
+ $code = key( $this->mruLangs );
+ wfDebug( __METHOD__.": unloading $code\n" );
+ $this->unload( $code );
+ }
+ }
+}
diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php
index 95109eb5..4bfdc2a3 100644
--- a/includes/LogEventsList.php
+++ b/includes/LogEventsList.php
@@ -39,7 +39,7 @@ class LogEventsList {
// Precache various messages
if( !isset( $this->message ) ) {
$messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink',
- 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'diff',
+ 'revertmove', 'undeletelink', 'undeleteviewlink', 'revdel-restore', 'hist', 'diff',
'pipe-separator' );
foreach( $messages as $msg ) {
$this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
@@ -49,18 +49,22 @@ class LogEventsList {
/**
* Set page title and show header for this log type
- * @param $type String
+ * @param $type Array
*/
public function showHeader( $type ) {
- if( LogPage::isLogType( $type ) ) {
- $this->out->setPageTitle( LogPage::logName( $type ) );
- $this->out->addHTML( LogPage::logHeader( $type ) );
+ // If only one log type is used, then show a special message...
+ $headerType = (count($type) == 1) ? $type[0] : '';
+ if( LogPage::isLogType( $headerType ) ) {
+ $this->out->setPageTitle( LogPage::logName( $headerType ) );
+ $this->out->addHTML( LogPage::logHeader( $headerType ) );
+ } else {
+ $this->out->addHTML( wfMsgExt('alllogstext',array('parseinline')) );
}
}
/**
* Show options for the log list
- * @param $type String
+ * @param $types string or Array
* @param $user String
* @param $page String
* @param $pattern String
@@ -69,32 +73,64 @@ class LogEventsList {
* @param $filter: array
* @param $tagFilter: array?
*/
- public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '',
- $month = '', $filter = null, $tagFilter='' )
+ public function showOptions( $types=array(), $user='', $page='', $pattern='', $year='',
+ $month = '', $filter = null, $tagFilter='' )
{
global $wgScript, $wgMiserMode;
- $action = htmlspecialchars( $wgScript );
+
+ $action = $wgScript;
$title = SpecialPage::getTitleFor( 'Log' );
- $special = htmlspecialchars( $title->getPrefixedDBkey() );
+ $special = $title->getPrefixedDBkey();
+
+ // For B/C, we take strings, but make sure they are converted...
+ $types = ($types === '') ? array() : (array)$types;
$tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
- $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>" . Xml::dateMenu( $year, $month ) . "\n" .
- ( $tagSelector ? Xml::tags( 'p', null, implode( '&nbsp;', $tagSelector ) ) :'' ). "\n" .
- ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) . "\n" .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" .
- "</fieldset></form>"
- );
+ $html = '';
+ $html .= Xml::hidden( 'title', $special );
+
+ // Basic selectors
+ $html .= $this->getTypeMenu( $types ) . "\n";
+ $html .= $this->getUserInput( $user ) . "\n";
+ $html .= $this->getTitleInput( $page ) . "\n";
+ $html .= $this->getExtraInputs( $types ) . "\n";
+
+ // Title pattern, if allowed
+ if (!$wgMiserMode) {
+ $html .= $this->getTitlePattern( $pattern ) . "\n";
+ }
+
+ // date menu
+ $html .= Xml::tags( 'p', null, Xml::dateMenu( $year, $month ) );
+
+ // Tag filter
+ if ($tagSelector) {
+ $html .= Xml::tags( 'p', null, implode( '&nbsp;', $tagSelector ) );
+ }
+
+ // Filter links
+ if ($filter) {
+ $html .= Xml::tags( 'p', null, $this->getFilterLinks( $filter ) );
+ }
+
+ // Submit button
+ $html .= Xml::submitButton( wfMsg( 'allpagessubmit' ) );
+
+ // Fieldset
+ $html = Xml::fieldset( wfMsg( 'log' ), $html );
+
+ // Form wrapping
+ $html = Xml::tags( 'form', array( 'action' => $action, 'method' => 'get' ), $html );
+
+ $this->out->addHTML( $html );
}
-
- private function getFilterLinks( $logType, $filter ) {
+
+ /**
+ * @param $filter Array
+ * @return String: Formatted HTML
+ */
+ private function getFilterLinks( $filter ) {
global $wgTitle, $wgLang;
// show/hide links
$messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) );
@@ -102,17 +138,29 @@ class LogEventsList {
$links = array();
$hiddens = ''; // keep track for "go" button
foreach( $filter as $type => $val ) {
+ // Should the below assignment be outside the foreach?
+ // Then it would have to be copied. Not certain what is more expensive.
+ $query = $this->getDefaultQuery();
+ $queryKey = "hide_{$type}_log";
+
$hideVal = 1 - intval($val);
- $link = $this->skin->makeKnownLinkObj( $wgTitle, $messages[$hideVal],
- wfArrayToCGI( array( "hide_{$type}_log" => $hideVal ), $this->getDefaultQuery() )
+ $query[$queryKey] = $hideVal;
+
+ $link = $this->skin->link(
+ $wgTitle,
+ $messages[$hideVal],
+ array(),
+ $query,
+ array( 'known', 'noclasses' )
);
+
$links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link );
$hiddens .= Xml::hidden( "hide_{$type}_log", $val ) . "\n";
}
// Build links
return '<small>'.$wgLang->pipeList( $links ) . '</small>' . $hiddens;
}
-
+
private function getDefaultQuery() {
if ( !isset( $this->mDefaultQuery ) ) {
$this->mDefaultQuery = $_GET;
@@ -128,10 +176,10 @@ class LogEventsList {
}
/**
- * @param $queryType String
+ * @param $queryTypes Array
* @return String: Formatted HTML
*/
- private function getTypeMenu( $queryType ) {
+ private function getTypeMenu( $queryTypes ) {
global $wgLogRestrictions, $wgUser;
$html = "<select name='type'>\n";
@@ -142,14 +190,16 @@ class LogEventsList {
// First pass to load the log names
foreach( $validTypes as $type ) {
$text = LogPage::logName( $type );
- $typesByName[$text] = $type;
+ $typesByName[$type] = $text;
}
// Second pass to sort by name
- ksort($typesByName);
+ asort($typesByName);
+ // Note the query type
+ $queryType = count($queryTypes) == 1 ? $queryTypes[0] : '';
// Third pass generates sorted XHTML content
- foreach( $typesByName as $text => $type ) {
+ foreach( $typesByName as $type => $text ) {
$selected = ($type == $queryType);
// Restricted types
if ( isset($wgLogRestrictions[$type]) ) {
@@ -170,7 +220,9 @@ class LogEventsList {
* @return String: Formatted HTML
*/
private function getUserInput( $user ) {
- return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user );
+ return '<span style="white-space: nowrap">' .
+ Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user ) .
+ '</span>';
}
/**
@@ -178,7 +230,9 @@ class LogEventsList {
* @return String: Formatted HTML
*/
private function getTitleInput( $title ) {
- return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title );
+ return '<span style="white-space: nowrap">' .
+ Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title ) .
+ '</span>';
}
/**
@@ -190,6 +244,20 @@ class LogEventsList {
'</span>';
}
+ private function getExtraInputs( $types ) {
+ global $wgRequest;
+ $offender = $wgRequest->getVal('offender');
+ $user = User::newFromName( $offender, false );
+ if( !$user || ($user->getId() == 0 && !IP::isIPAddress($offender) ) ) {
+ $offender = ''; // Blank field if invalid
+ }
+ if( count($types) == 1 && $types[0] == 'suppress' ) {
+ return Xml::inputLabel( wfMsg('revdelete-offender'), 'offender',
+ 'mw-log-offender', 20, $offender );
+ }
+ return '';
+ }
+
public function beginLogEventsList() {
return "<ul>\n";
}
@@ -207,17 +275,17 @@ class LogEventsList {
$title = Title::makeTitle( $row->log_namespace, $row->log_title );
$classes = array( "mw-logline-{$row->log_type}" );
- $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->log_timestamp), true );
+ $time = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->log_timestamp ), true );
// User links
- if( self::isDeleted($row,LogPage::DELETED_USER) ) {
+ 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>';
+ 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 );
}
@@ -225,46 +293,79 @@ class LogEventsList {
$paramArray = LogPage::extractParams( $row->log_params );
$revert = $del = '';
// Some user can hide log items and have review links
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
- $del = $this->getShowHideLinks( $row ) . ' ';
+ if( !( $this->flags & self::NO_ACTION_LINK ) && $wgUser->isAllowed( 'deletedhistory' ) ) {
+ // Don't show useless link to people who cannot hide revisions
+ if( $row->log_deleted || $wgUser->isAllowed( 'deleterevision' ) ) {
+ $del = $this->getShowHideLinks( $row ) . ' ';
+ }
}
// Add review links and such...
- if( ($this->flags & self::NO_ACTION_LINK) || ($row->log_deleted & LogPage::DELETED_ACTION) ) {
+ if( ( $this->flags & self::NO_ACTION_LINK ) || ( $row->log_deleted & LogPage::DELETED_ACTION ) ) {
// Action text is suppressed...
- } else if( self::typeAction($row,'move','move','move') && !empty($paramArray[0]) ) {
+ } else if( self::typeAction( $row, 'move', 'move', 'move' ) && !empty( $paramArray[0] ) ) {
$destTitle = Title::newFromText( $paramArray[0] );
if( $destTitle ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
+ $revert = '(' . $this->skin->link(
+ SpecialPage::getTitleFor( 'Movepage' ),
$this->message['revertmove'],
- 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
- '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
- '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
- '&wpMovetalk=0' ) . ')';
+ array(),
+ array(
+ 'wpOldTitle' => $destTitle->getPrefixedDBkey(),
+ 'wpNewTitle' => $title->getPrefixedDBkey(),
+ 'wpReason' => wfMsgForContent( 'revertmove' ),
+ 'wpMovetalk' => 0
+ ),
+ array( 'known', 'noclasses' )
+ ) . ')';
}
// Show undelete link
- } else if( self::typeAction($row,array('delete','suppress'),'delete','delete') ) {
- $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
- $this->message['undeletelink'], 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
+ } else if( self::typeAction( $row, array( 'delete', 'suppress' ), 'delete', 'deletedhistory' ) ) {
+ if( !$wgUser->isAllowed( 'undelete' ) ) {
+ $viewdeleted = $this->message['undeleteviewlink'];
+ } else {
+ $viewdeleted = $this->message['undeletelink'];
+ }
+
+ $revert = '(' . $this->skin->link(
+ SpecialPage::getTitleFor( 'Undelete' ),
+ $viewdeleted,
+ array(),
+ array( 'target' => $title->getPrefixedDBkey() ),
+ array( 'known', 'noclasses' )
+ ) . ')';
// Show unblock/change block link
- } else if( self::typeAction($row,array('block','suppress'),array('block','reblock'),'block') ) {
+ } else if( self::typeAction( $row, array( 'block', 'suppress' ), array( 'block', 'reblock' ), 'block' ) ) {
$revert = '(' .
- $this->skin->link( SpecialPage::getTitleFor( 'Ipblocklist' ),
+ $this->skin->link(
+ SpecialPage::getTitleFor( 'Ipblocklist' ),
$this->message['unblocklink'],
array(),
- array( 'action' => 'unblock', 'ip' => $row->log_title ),
- 'known' )
- . $this->message['pipe-separator'] .
- $this->skin->link( SpecialPage::getTitleFor( 'Blockip', $row->log_title ),
+ array(
+ 'action' => 'unblock',
+ 'ip' => $row->log_title
+ ),
+ 'known'
+ ) .
+ $this->message['pipe-separator'] .
+ $this->skin->link(
+ SpecialPage::getTitleFor( 'Blockip', $row->log_title ),
$this->message['change-blocklink'],
- array(), array(), 'known' ) .
+ array(),
+ array(),
+ 'known'
+ ) .
')';
// Show change protection link
} else if( self::typeAction( $row, 'protect', array( 'modify', 'protect', 'unprotect' ) ) ) {
- $revert .= ' (' .
+ $revert .= ' (' .
$this->skin->link( $title,
$this->message['hist'],
array(),
- array( 'action' => 'history', 'offset' => $row->log_timestamp ) );
+ array(
+ 'action' => 'history',
+ 'offset' => $row->log_timestamp
+ )
+ );
if( $wgUser->isAllowed( 'protect' ) ) {
$revert .= $this->message['pipe-separator'] .
$this->skin->link( $title,
@@ -275,50 +376,94 @@ class LogEventsList {
}
$revert .= ')';
// Show unmerge link
- } else if( self::typeAction($row,'merge','merge','mergehistory') ) {
+ } else if( self::typeAction( $row, 'merge', 'merge', 'mergehistory' ) ) {
$merge = SpecialPage::getTitleFor( 'Mergehistory' );
- $revert = '(' . $this->skin->makeKnownLinkObj( $merge, $this->message['revertmerge'],
- wfArrayToCGI( array('target' => $paramArray[0], 'dest' => $title->getPrefixedDBkey(),
- 'mergepoint' => $paramArray[1] ) ) ) . ')';
+ $revert = '(' . $this->skin->link(
+ $merge,
+ $this->message['revertmerge'],
+ array(),
+ array(
+ 'target' => $paramArray[0],
+ 'dest' => $title->getPrefixedDBkey(),
+ 'mergepoint' => $paramArray[1]
+ ),
+ array( 'known', 'noclasses' )
+ ) . ')';
// If an edit was hidden from a page give a review link to the history
- } else if( self::typeAction($row,array('delete','suppress'),'revision','deleterevision') ) {
- if( count($paramArray) == 2 ) {
- $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ } else if( self::typeAction( $row, array( 'delete', 'suppress' ), 'revision', 'deletedhistory' ) ) {
+ if( count($paramArray) >= 2 ) {
// Different revision types use different URL params...
$key = $paramArray[0];
- // Link to each hidden object ID, $paramArray[1] is the url param
+ // $paramArray[1] is a CSV of the IDs
$Ids = explode( ',', $paramArray[1] );
- $revParams = '';
- foreach( $Ids as $n => $id ) {
- $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id);
- }
+ $query = $paramArray[1];
$revert = array();
// Diff link for single rev deletions
- if( $key === 'oldid' && count($Ids) == 1 ) {
- $token = urlencode( $wgUser->editToken( intval($Ids[0]) ) );
- $revert[] = $this->skin->makeKnownLinkObj( $title, $this->message['diff'],
- 'diff='.intval($Ids[0])."&unhide=1&token=$token" );
+ if( count($Ids) == 1 ) {
+ // Live revision diffs...
+ if( in_array( $key, array( 'oldid', 'revision' ) ) ) {
+ $revert[] = $this->skin->link(
+ $title,
+ $this->message['diff'],
+ array(),
+ array(
+ 'diff' => intval( $Ids[0] ),
+ 'unhide' => 1
+ ),
+ array( 'known', 'noclasses' )
+ );
+ // Deleted revision diffs...
+ } else if( in_array( $key, array( 'artimestamp','archive' ) ) ) {
+ $revert[] = $this->skin->link(
+ SpecialPage::getTitleFor( 'Undelete' ),
+ $this->message['diff'],
+ array(),
+ array(
+ 'target' => $title->getPrefixedDBKey(),
+ 'diff' => 'prev',
+ 'timestamp' => $Ids[0]
+ ),
+ array( 'known', 'noclasses' )
+ );
+ }
}
// View/modify link...
- $revert[] = $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'],
- 'target=' . $title->getPrefixedUrl() . $revParams );
- $revert = '(' . implode(' | ',$revert) . ')';
+ $revert[] = $this->skin->link(
+ SpecialPage::getTitleFor( 'Revisiondelete' ),
+ $this->message['revdel-restore'],
+ array(),
+ array(
+ 'target' => $title->getPrefixedText(),
+ 'type' => $key,
+ 'ids' => $query
+ ),
+ array( 'known', 'noclasses' )
+ );
+ // Pipe links
+ $revert = wfMsg( 'parentheses', $wgLang->pipeList( $revert ) );
}
// Hidden log items, give review link
- } else if( self::typeAction($row,array('delete','suppress'),'event','deleterevision') ) {
- if( count($paramArray) == 1 ) {
+ } else if( self::typeAction( $row, array( 'delete', 'suppress' ), 'event', 'deletedhistory' ) ) {
+ if( count($paramArray) >= 1 ) {
$revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ // $paramArray[1] is a CSV of the IDs
$Ids = explode( ',', $paramArray[0] );
+ $query = $paramArray[0];
// Link to each hidden object ID, $paramArray[1] is the url param
- $logParams = '';
- foreach( $Ids as $n => $id ) {
- $logParams .= '&logid[]=' . intval($id);
- }
- $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'],
- 'target=' . $title->getPrefixedUrl() . $logParams ) . ')';
+ $revert = '(' . $this->skin->link(
+ $revdel,
+ $this->message['revdel-restore'],
+ array(),
+ array(
+ 'target' => $title->getPrefixedText(),
+ 'type' => 'logging',
+ 'ids' => $query
+ ),
+ array( 'known', 'noclasses' )
+ ) . ')';
}
// Self-created users
- } else if( self::typeAction($row,'newusers','create2') ) {
+ } else if( self::typeAction( $row, 'newusers', 'create2' ) ) {
if( isset( $paramArray[0] ) ) {
$revert = $this->skin->userToolLinks( $paramArray[0], $title->getDBkey(), true );
} else {
@@ -336,21 +481,23 @@ class LogEventsList {
&$comment, &$revert, $row->log_timestamp ) );
}
// Event description
- if( self::isDeleted($row,LogPage::DELETED_ACTION) ) {
- $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
+ 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 );
}
// Any tags...
- list($tagDisplay, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' );
+ list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' );
$classes = array_merge( $classes, $newClasses );
if( $revert != '' ) {
$revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>';
}
+ $time = htmlspecialchars( $time );
+
return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ),
$del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert . " $tagDisplay" ) . "\n";
}
@@ -360,19 +507,24 @@ class LogEventsList {
* @return string
*/
private function getShowHideLinks( $row ) {
- $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+ global $wgUser;
+ if( $row->log_type == 'suppress' ) {
+ return ''; // No one can hide items from the oversight log
+ }
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
// If event was hidden from sysops
if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) {
- $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' );
- } else if( $row->log_type == 'suppress' ) {
- // No one should be hiding from the oversight log
- $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' );
+ $del = $this->skin->revDeleteLinkDisabled( $canHide );
} else {
$target = SpecialPage::getTitleFor( 'Log', $row->log_type );
- $query = array( 'target' => $target->getPrefixedDBkey(),
- 'logid[]' => $row->log_id
+ $page = Title::makeTitle( $row->log_namespace, $row->log_title );
+ $query = array(
+ 'target' => $target->getPrefixedDBkey(),
+ 'type' => 'logging',
+ 'ids' => $row->log_id,
);
- $del = $this->skin->revDeleteLink( $query, self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) );
+ $del = $this->skin->revDeleteLink( $query,
+ self::isDeleted( $row, LogPage::DELETED_RESTRICTED ), $canHide );
}
return $del;
}
@@ -385,10 +537,11 @@ class LogEventsList {
* @return bool
*/
public static function typeAction( $row, $type, $action, $right='' ) {
- $match = is_array($type) ? in_array($row->log_type,$type) : $row->log_type == $type;
+ $match = is_array($type) ?
+ in_array( $row->log_type, $type ) : $row->log_type == $type;
if( $match ) {
- $match = is_array($action) ?
- in_array($row->log_action,$action) : $row->log_action == $action;
+ $match = is_array( $action ) ?
+ in_array( $row->log_action, $action ) : $row->log_action == $action;
if( $match && $right ) {
global $wgUser;
$match = $wgUser->isAllowed( $right );
@@ -405,12 +558,26 @@ class LogEventsList {
* @return Boolean
*/
public static function userCan( $row, $field ) {
- if( ( $row->log_deleted & $field ) == $field ) {
+ return self::userCanBitfield( $row->log_deleted, $field );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this log row, if it's marked as deleted.
+ * @param $bitfield Integer (current field)
+ * @param $field Integer
+ * @return Boolean
+ */
+ public static function userCanBitfield( $bitfield, $field ) {
+ if( $bitfield & $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" );
+ $permission = '';
+ if ( $bitfield & LogPage::DELETED_RESTRICTED ) {
+ $permission = 'suppressrevision';
+ } else {
+ $permission = 'deletedhistory';
+ }
+ wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
return $wgUser->isAllowed( $permission );
} else {
return true;
@@ -423,33 +590,111 @@ class LogEventsList {
* @return Boolean
*/
public static function isDeleted( $row, $field ) {
- return ($row->log_deleted & $field) == $field;
+ return ( $row->log_deleted & $field ) == $field;
}
/**
- * Quick function to show a short log extract
- * @param $out OutputPage
- * @param $type String
- * @param $page String
- * @param $user String
- * @param $lim Integer
- * @param $conds Array
+ * Show log extract. Either with text and a box (set $msgKey) or without (don't set $msgKey)
+ * @param $out OutputPage or String-by-reference
+ * @param $types String or Array
+ * @param $page String The page title to show log entries for
+ * @param $user String The user who made the log entries
+ * @param $param Associative Array with the following additional options:
+ * - lim Integer Limit of items to show, default is 50
+ * - conds Array Extra conditions for the query (e.g. "log_action != 'revision'")
+ * - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty
+ * if set to true (default), "No matching items in log" is displayed if loglist is empty
+ * - msgKey Array If you want a nice box with a message, set this to the key of the message.
+ * First element is the message key, additional optional elements are parameters for the key
+ * that are processed with wgMsgExt and option 'parse'
+ * - offset Set to overwrite offset parameter in $wgRequest
+ * set to '' to unset offset
+ * - wrap String: Wrap the message in html (usually something like "<div ...>$1</div>").
+ * @return Integer Number of total log items (not limited by $lim)
*/
- public static function showLogExtract( $out, $type='', $page='', $user='', $lim=0, $conds=array() ) {
- global $wgUser;
- # Insert list of top 50 or so items
- $loglist = new LogEventsList( $wgUser->getSkin(), $out, 0 );
- $pager = new LogPager( $loglist, $type, $user, $page, '', $conds );
+ public static function showLogExtract( &$out, $types=array(), $page='', $user='',
+ $param = array() ) {
+
+ $defaultParameters = array(
+ 'lim' => 25,
+ 'conds' => array(),
+ 'showIfEmpty' => true,
+ 'msgKey' => array(''),
+ 'wrap' => "$1"
+ );
+
+ # The + operator appends elements of remaining keys from the right
+ # handed array to the left handed, whereas duplicated keys are NOT overwritten.
+ $param += $defaultParameters;
+
+ global $wgUser, $wgOut;
+ # Convert $param array to individual variables
+ $lim = $param['lim'];
+ $conds = $param['conds'];
+ $showIfEmpty = $param['showIfEmpty'];
+ $msgKey = $param['msgKey'];
+ $wrap = $param['wrap'];
+ if ( !is_array( $msgKey ) )
+ $msgKey = array( $msgKey );
+ # Insert list of top 50 (or top $lim) items
+ $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 );
+ $pager = new LogPager( $loglist, $types, $user, $page, '', $conds );
+ if ( isset( $param['offset'] ) ) # Tell pager to ignore $wgRequest offset
+ $pager->setOffset( $param['offset'] );
if( $lim > 0 ) $pager->mLimit = $lim;
$logBody = $pager->getBody();
+ $s = '';
if( $logBody ) {
- $out->addHTML(
- $loglist->beginLogEventsList() .
- $logBody .
- $loglist->endLogEventsList()
+ if ( $msgKey[0] ) {
+ $s = '<div class="mw-warning-with-logexcerpt">';
+
+ if ( count( $msgKey ) == 1 ) {
+ $s .= wfMsgExt( $msgKey[0], array( 'parse' ) );
+ } else { // Process additional arguments
+ $args = $msgKey;
+ array_shift( $args );
+ $s .= wfMsgExt( $msgKey[0], array( 'parse' ), $args );
+ }
+ }
+ $s .= $loglist->beginLogEventsList() .
+ $logBody .
+ $loglist->endLogEventsList();
+ } else {
+ if ( $showIfEmpty )
+ $s = Html::rawElement( 'div', array( 'class' => 'mw-warning-logempty' ),
+ wfMsgExt( 'logempty', array( 'parseinline' ) ) );
+ }
+ if( $pager->getNumRows() > $pager->mLimit ) { # Show "Full log" link
+ $urlParam = array();
+ if ( $page != '')
+ $urlParam['page'] = $page;
+ if ( $user != '')
+ $urlParam['user'] = $user;
+ if ( !is_array( $types ) ) # Make it an array, if it isn't
+ $types = array( $types );
+ # If there is exactly one log type, we can link to Special:Log?type=foo
+ if ( count( $types ) == 1 )
+ $urlParam['type'] = $types[0];
+ $s .= $wgUser->getSkin()->link(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'log-fulllog' ),
+ array(),
+ $urlParam
);
+
+ }
+ if ( $logBody && $msgKey[0] )
+ $s .= '</div>';
+
+ if ( $wrap!='' ) { // Wrap message in html
+ $s = str_replace( '$1', $s, $wrap );
+ }
+
+ // $out can be either an OutputPage object or a String-by-reference
+ if( $out instanceof OutputPage ){
+ $out->addHTML( $s );
} else {
- $out->addWikiMsg( 'logempty' );
+ $out = $s;
}
return $pager->getNumRows();
}
@@ -484,21 +729,22 @@ class LogEventsList {
* @ingroup Pager
*/
class LogPager extends ReverseChronologicalPager {
- private $type = '', $user = '', $title = '', $pattern = '';
+ private $types = array(), $user = '', $title = '', $pattern = '';
+ private $typeCGI = '';
public $mLogEventsList;
/**
* constructor
* @param $list LogEventsList
- * @param $type String
- * @param $user String
- * @param $title String
- * @param $pattern String
- * @param $conds Array
- * @param $year Integer
- * @param $month Integer
+ * @param $types String or Array log types to show
+ * @param $user String The user who made the log entries
+ * @param $title String The page title the log entries are for
+ * @param $pattern String Do a prefix search rather than an exact title match
+ * @param $conds Array Extra conditions for the query
+ * @param $year Integer The year to start from
+ * @param $month Integer The month to start from
*/
- public function __construct( $list, $type = '', $user = '', $title = '', $pattern = '',
+ public function __construct( $list, $types = array(), $user = '', $title = '', $pattern = '',
$conds = array(), $year = false, $month = false, $tagFilter = '' )
{
parent::__construct();
@@ -506,7 +752,7 @@ class LogPager extends ReverseChronologicalPager {
$this->mLogEventsList = $list;
- $this->limitType( $type ); // also excludes hidden types
+ $this->limitType( $types ); // also excludes hidden types
$this->limitUser( $user );
$this->limitTitle( $title, $pattern );
$this->getDateCond( $year, $month );
@@ -515,17 +761,18 @@ class LogPager extends ReverseChronologicalPager {
public function getDefaultQuery() {
$query = parent::getDefaultQuery();
- $query['type'] = $this->type;
+ $query['type'] = $this->typeCGI; // arrays won't work here
$query['user'] = $this->user;
$query['month'] = $this->mMonth;
$query['year'] = $this->mYear;
return $query;
}
+ // Call ONLY after calling $this->limitType() already!
public function getFilterParams() {
global $wgFilterLogTypes, $wgUser, $wgRequest;
$filters = array();
- if( $this->type ) {
+ if( count($this->types) ) {
return $filters;
}
foreach( $wgFilterLogTypes as $type => $default ) {
@@ -543,24 +790,33 @@ class LogPager extends ReverseChronologicalPager {
/**
* Set the log reader to return only entries of the given type.
* Type restrictions enforced here
- * @param $type String: A log type ('upload', 'delete', etc)
+ * @param $types String or array: Log types ('upload', 'delete', etc);
+ * empty string means no restriction
*/
- private function limitType( $type ) {
+ private function limitType( $types ) {
global $wgLogRestrictions, $wgUser;
+ // If $types is not an array, make it an array
+ $types = ($types === '') ? array() : (array)$types;
// Don't even show header for private logs; don't recognize it...
- if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed($wgLogRestrictions[$type]) ) {
- $type = '';
+ foreach ( $types as $type ) {
+ if( isset( $wgLogRestrictions[$type] )
+ && !$wgUser->isAllowed($wgLogRestrictions[$type])
+ ) {
+ $types = array_diff( $types, array( $type ) );
+ }
}
- // Don't show private logs to unpriviledged users.
+ $this->types = $types;
+ // Don't show private logs to unprivileged users.
// Also, only show them upon specific request to avoid suprises.
- $audience = $type ? 'user' : 'public';
+ $audience = $types ? 'user' : 'public';
$hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience );
if( $hideLogs !== false ) {
$this->mConds[] = $hideLogs;
}
- if( $type ) {
- $this->type = $type;
- $this->mConds['log_type'] = $type;
+ if( count($types) ) {
+ $this->mConds['log_type'] = $types;
+ // Set typeCGI; used in url param for paging
+ if( count($types) == 1 ) $this->typeCGI = $types[0];
}
}
@@ -586,8 +842,11 @@ class LogPager extends ReverseChronologicalPager {
global $wgUser;
$this->mConds['log_user'] = $userid;
// Paranoia: avoid brute force searches (bug 17342)
- if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
- $this->mConds[] = 'log_deleted & ' . LogPage::DELETED_USER . ' = 0';
+ if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::DELETED_USER) . ' = 0';
+ } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
+ $this->mConds[] = $this->mDb->bitAnd('log_deleted', LogPage::SUPPRESSED_USER) .
+ ' != ' . LogPage::SUPPRESSED_USER;
}
$this->user = $usertitle->getText();
}
@@ -603,11 +862,13 @@ class LogPager extends ReverseChronologicalPager {
global $wgMiserMode, $wgUser;
$title = Title::newFromText( $page );
- if( strlen($page) == 0 || !$title instanceof Title )
+ if( strlen( $page ) == 0 || !$title instanceof Title )
return false;
$this->title = $title->getPrefixedText();
$ns = $title->getNamespace();
+ $db = $this->mDb;
+
# 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
@@ -620,43 +881,64 @@ class LogPager extends ReverseChronologicalPager {
# 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->mConds[] = 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() );
$this->pattern = $pattern;
} else {
$this->mConds['log_namespace'] = $ns;
$this->mConds['log_title'] = $title->getDBkey();
}
// Paranoia: avoid brute force searches (bug 17342)
- if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
- $this->mConds[] = 'log_deleted & ' . LogPage::DELETED_ACTION . ' = 0';
+ if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
+ $this->mConds[] = $db->bitAnd('log_deleted', LogPage::DELETED_ACTION) . ' = 0';
+ } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
+ $this->mConds[] = $db->bitAnd('log_deleted', LogPage::SUPPRESSED_ACTION) .
+ ' != ' . LogPage::SUPPRESSED_ACTION;
}
}
public function getQueryInfo() {
+ global $wgOut;
+ $tables = array( 'logging', 'user' );
$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' ) );
+ $index = array();
+ $options = array();
+ # Add log_search table if there are conditions on it
+ if( array_key_exists('ls_field',$this->mConds) ) {
+ $tables[] = 'log_search';
+ $index['log_search'] = 'ls_field_val';
+ $index['logging'] = 'PRIMARY';
+ $options[] = 'DISTINCT';
+ # Avoid usage of the wrong index by limiting
+ # the choices of available indexes. This mainly
+ # avoids site-breaking filesorts.
+ } else if( $this->title || $this->pattern || $this->user ) {
+ $index['logging'] = array( 'page_time', 'user_time' );
+ if( count($this->types) == 1 ) {
+ $index['logging'][] = 'log_user_type_time';
+ }
+ } else if( count($this->types) == 1 ) {
+ $index['logging'] = 'type_time';
} else {
- $index = array( 'USE INDEX' => array( 'logging' => 'times' ) );
+ $index['logging'] = 'times';
}
+ $options['USE INDEX'] = $index;
+ # Don't show duplicate rows when using log_search
$info = 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,
- 'join_conds' => array( 'user' => array( 'INNER JOIN', 'user_id=log_user' ) ),
+ 'tables' => $tables,
+ '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' => $options,
+ 'join_conds' => array(
+ 'user' => array( 'INNER JOIN', 'user_id=log_user' ),
+ 'log_search' => array( 'INNER JOIN', 'ls_log_id=log_id' )
+ )
);
-
+ # Add ChangeTags filter query
ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'],
$info['join_conds'], $info['options'], $this->mTagFilter );
-
return $info;
}
@@ -686,7 +968,7 @@ class LogPager extends ReverseChronologicalPager {
}
public function getType() {
- return $this->type;
+ return $this->types;
}
public function getUser() {
@@ -712,6 +994,13 @@ class LogPager extends ReverseChronologicalPager {
public function getTagFilter() {
return $this->mTagFilter;
}
+
+ public function doQuery() {
+ // Workaround MySQL optimizer bug
+ $this->mDb->setBigSelects();
+ parent::doQuery();
+ $this->mDb->setBigSelects( 'default' );
+ }
}
/**
@@ -772,7 +1061,6 @@ class LogViewer {
* LogEventsList::NO_ACTION_LINK Don't show restore/unblock/block links
*/
function __construct( &$reader, $flags = 0 ) {
- global $wgUser;
wfDeprecated(__METHOD__);
$this->reader =& $reader;
$this->reader->pager->mLogEventsList->flags = $flags;
diff --git a/includes/LogPage.php b/includes/LogPage.php
index 0d572385..1d8d6c1c 100644
--- a/includes/LogPage.php
+++ b/includes/LogPage.php
@@ -33,7 +33,10 @@ class LogPage {
const DELETED_ACTION = 1;
const DELETED_COMMENT = 2;
const DELETED_USER = 4;
- const DELETED_RESTRICTED = 8;
+ const DELETED_RESTRICTED = 8;
+ // Convenience fields
+ const SUPPRESSED_USER = 12;
+ const SUPPRESSED_ACTION = 9;
/* @access private */
var $type, $action, $comment, $params, $target, $doer;
/* @acess public */
@@ -57,7 +60,7 @@ class LogPage {
global $wgLogRestrictions;
$dbw = wfGetDB( DB_MASTER );
- $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' );
+ $log_id = $dbw->nextSequenceValue( 'logging_log_id_seq' );
$this->timestamp = $now = wfTimestampNow();
$data = array(
@@ -66,8 +69,10 @@ class LogPage {
'log_action' => $this->action,
'log_timestamp' => $dbw->timestamp( $now ),
'log_user' => $this->doer->getId(),
+ 'log_user_text' => $this->doer->getName(),
'log_namespace' => $this->target->getNamespace(),
'log_title' => $this->target->getDBkey(),
+ 'log_page' => $this->target->getArticleId(),
'log_comment' => $this->comment,
'log_params' => $this->params
);
@@ -91,7 +96,7 @@ class LogPage {
$this->type, $this->action, $this->target, $this->comment, $this->params, $newId );
$rc->notifyRC2UDP();
}
- return true;
+ return $newId;
}
/**
@@ -99,7 +104,7 @@ class LogPage {
*/
public function getRcComment() {
$rcComment = $this->actionText;
- if( '' != $this->comment ) {
+ if( $this->comment != '' ) {
if ($rcComment == '')
$rcComment = $this->comment;
else
@@ -132,6 +137,7 @@ class LogPage {
/**
* @static
+ * @param string $type logtype
*/
public static function logName( $type ) {
global $wgLogNames, $wgMessageCache;
@@ -160,7 +166,7 @@ class LogPage {
* @static
* @return HTML string
*/
- public static function actionText( $type, $action, $title = NULL, $skin = NULL,
+ public static function actionText( $type, $action, $title = null, $skin = null,
$params = array(), $filterWikilinks = false )
{
global $wgLang, $wgContLang, $wgLogActions, $wgMessageCache;
@@ -173,7 +179,7 @@ class LogPage {
}
if( isset( $wgLogActions[$key] ) ) {
if( is_null( $title ) ) {
- $rv = wfMsg( $wgLogActions[$key] );
+ $rv = wfMsgHtml( $wgLogActions[$key] );
} else {
$titleLink = self::getTitleLink( $type, $skin, $title, $params );
if( $key == 'rights/rights' ) {
@@ -194,13 +200,14 @@ class LogPage {
}
if( count( $params ) == 0 ) {
if ( $skin ) {
- $rv = wfMsg( $wgLogActions[$key], $titleLink );
+ $rv = wfMsgHtml( $wgLogActions[$key], $titleLink );
} else {
- $rv = wfMsgForContent( $wgLogActions[$key], $titleLink );
+ $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'replaceafter', 'content' ), $titleLink );
}
} else {
$details = '';
array_unshift( $params, $titleLink );
+ // User suppression
if ( preg_match( '/^(block|suppress)\/(block|reblock)$/', $key ) ) {
if ( $skin ) {
$params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' .
@@ -210,8 +217,15 @@ class LogPage {
}
$params[2] = isset( $params[2] ) ?
self::formatBlockFlags( $params[2], is_null( $skin ) ) : '';
+ // Page protections
} else if ( $type == 'protect' && count($params) == 3 ) {
- $details .= " {$params[1]}"; // restrictions and expiries
+ // Restrictions and expiries
+ if( $skin ) {
+ $details .= htmlspecialchars( " {$params[1]}" );
+ } else {
+ $details .= " {$params[1]}";
+ }
+ // Cascading flag...
if( $params[2] ) {
if ( $skin ) {
$details .= ' ['.wfMsg('protect-summary-cascade').']';
@@ -219,6 +233,7 @@ class LogPage {
$details .= ' ['.wfMsgForContent('protect-summary-cascade').']';
}
}
+ // Page moves
} else if ( $type == 'move' && count( $params ) == 3 ) {
if( $params[2] ) {
if ( $skin ) {
@@ -227,8 +242,24 @@ class LogPage {
$details .= ' [' . wfMsgForContent( 'move-redirect-suppressed' ) . ']';
}
}
+ // Revision deletion
+ } else if ( preg_match( '/^(delete|suppress)\/revision$/', $key ) && count( $params ) == 5 ) {
+ $count = substr_count( $params[2], ',' ) + 1; // revisions
+ $ofield = intval( substr( $params[3], 7 ) ); // <ofield=x>
+ $nfield = intval( substr( $params[4], 7 ) ); // <nfield=x>
+ $details .= ': '.RevisionDeleter::getLogMessage( $count, $nfield, $ofield, false );
+ // Log deletion
+ } else if ( preg_match( '/^(delete|suppress)\/event$/', $key ) && count( $params ) == 4 ) {
+ $count = substr_count( $params[1], ',' ) + 1; // log items
+ $ofield = intval( substr( $params[2], 7 ) ); // <ofield=x>
+ $nfield = intval( substr( $params[3], 7 ) ); // <nfield=x>
+ $details .= ': '.RevisionDeleter::getLogMessage( $count, $nfield, $ofield, true );
+ }
+ if ( $skin ) {
+ $rv = wfMsgHtml( $wgLogActions[$key], $params ) . $details;
+ } else {
+ $rv = wfMsgExt( $wgLogActions[$key], array( 'parsemag', 'escape', 'replaceafter', 'content' ), $params ) . $details;
}
- $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ) . $details;
}
}
} else {
@@ -260,20 +291,27 @@ class LogPage {
}
protected static function getTitleLink( $type, $skin, $title, &$params ) {
- global $wgLang, $wgContLang;
+ global $wgLang, $wgContLang, $wgUserrightsInterwikiDelimiter;
if( !$skin ) {
return $title->getPrefixedText();
}
switch( $type ) {
case 'move':
- $titleLink = $skin->makeLinkObj( $title,
- htmlspecialchars( $title->getPrefixedText() ), 'redirect=no' );
+ $titleLink = $skin->link(
+ $title,
+ htmlspecialchars( $title->getPrefixedText() ),
+ array(),
+ array( 'redirect' => 'no' )
+ );
$targetTitle = Title::newFromText( $params[0] );
if ( !$targetTitle ) {
# Workaround for broken database
$params[0] = htmlspecialchars( $params[0] );
} else {
- $params[0] = $skin->makeLinkObj( $targetTitle, htmlspecialchars( $params[0] ) );
+ $params[0] = $skin->link(
+ $targetTitle,
+ htmlspecialchars( $params[0] )
+ );
}
break;
case 'block':
@@ -289,24 +327,39 @@ class LogPage {
break;
case 'rights':
$text = $wgContLang->ucfirst( $title->getText() );
- $titleLink = $skin->makeLinkObj( Title::makeTitle( NS_USER, $text ) );
+ $parts = explode( $wgUserrightsInterwikiDelimiter, $text, 2 );
+ if ( count( $parts ) == 2 ) {
+ $titleLink = WikiMap::foreignUserLink( $parts[1], $parts[0],
+ htmlspecialchars( $title->getPrefixedText() ) );
+ if ( $titleLink !== false )
+ break;
+ }
+ $titleLink = $skin->link( Title::makeTitle( NS_USER, $text ) );
break;
case 'merge':
- $titleLink = $skin->makeLinkObj( $title, $title->getPrefixedText(), 'redirect=no' );
- $params[0] = $skin->makeLinkObj( Title::newFromText( $params[0] ), htmlspecialchars( $params[0] ) );
+ $titleLink = $skin->link(
+ $title,
+ $title->getPrefixedText(),
+ array(),
+ array( 'redirect' => 'no' )
+ );
+ $params[0] = $skin->link(
+ Title::newFromText( $params[0] ),
+ htmlspecialchars( $params[0] )
+ );
$params[1] = $wgLang->timeanddate( $params[1] );
break;
default:
if( $title->getNamespace() == NS_SPECIAL ) {
- list( $name, $par ) = SpecialPage::resolveAliasWithSubpage( $title->getDBKey() );
+ list( $name, $par ) = SpecialPage::resolveAliasWithSubpage( $title->getDBkey() );
# Use the language name for log titles, rather than Log/X
if( $name == 'Log' ) {
- $titleLink = '('.$skin->makeLinkObj( $title, LogPage::logName( $par ) ).')';
+ $titleLink = '('.$skin->link( $title, LogPage::logName( $par ) ).')';
} else {
- $titleLink = $skin->makeLinkObj( $title );
+ $titleLink = $skin->link( $title );
}
} else {
- $titleLink = $skin->makeLinkObj( $title );
+ $titleLink = $skin->link( $title );
}
}
return $titleLink;
@@ -325,6 +378,8 @@ class LogPage {
$params = array( $params );
}
+ if ( $comment === null ) $comment = "";
+
$this->action = $action;
$this->target = $target;
$this->comment = $comment;
@@ -339,10 +394,26 @@ class LogPage {
$this->doer = $doer;
- $this->actionText = LogPage::actionText( $this->type, $action, $target, NULL, $params );
+ $this->actionText = LogPage::actionText( $this->type, $action, $target, null, $params );
return $this->saveContent();
}
+
+ /**
+ * Add relations to log_search table
+ * @static
+ */
+ public function addRelations( $field, $values, $logid ) {
+ if( !strlen($field) || empty($values) )
+ return false; // nothing
+ $data = array();
+ foreach( $values as $value ) {
+ $data[] = array('ls_field' => $field,'ls_value' => $value,'ls_log_id' => $logid);
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->insert( 'log_search', $data, __METHOD__, 'IGNORE' );
+ return true;
+ }
/**
* Create a blob from a parameter array
diff --git a/includes/MagicWord.php b/includes/MagicWord.php
index 4e97016d..d741832f 100644
--- a/includes/MagicWord.php
+++ b/includes/MagicWord.php
@@ -36,6 +36,7 @@ class MagicWord {
static public $mVariableIDsInitialised = false;
static public $mVariableIDs = array(
'currentmonth',
+ 'currentmonth1',
'currentmonthname',
'currentmonthnamegen',
'currentmonthabbrev',
@@ -46,6 +47,7 @@ class MagicWord {
'currenttime',
'currenthour',
'localmonth',
+ 'localmonth1',
'localmonthname',
'localmonthnamegen',
'localmonthabbrev',
@@ -62,6 +64,7 @@ class MagicWord {
'server',
'servername',
'scriptpath',
+ 'stylepath',
'pagename',
'pagenamee',
'fullpagename',
@@ -81,7 +84,6 @@ class MagicWord {
'revisionuser',
'subpagename',
'subpagenamee',
- 'displaytitle',
'talkspace',
'talkspacee',
'subjectspace',
@@ -92,31 +94,22 @@ class MagicWord {
'subjectpagenamee',
'numberofusers',
'numberofactiveusers',
- 'newsectionlink',
- 'nonewsectionlink',
'numberofpages',
'currentversion',
'basepagename',
'basepagenamee',
- 'urlencode',
'currenttimestamp',
'localtimestamp',
'directionmark',
- 'language',
'contentlanguage',
- 'pagesinnamespace',
'numberofadmins',
'numberofviews',
- 'defaultsort',
- 'pagesincategory',
- 'index',
- 'noindex',
- 'numberingroup',
);
/* Array of caching hints for ParserCache */
static public $mCacheTTLs = array (
'currentmonth' => 86400,
+ 'currentmonth1' => 86400,
'currentmonthname' => 86400,
'currentmonthnamegen' => 86400,
'currentmonthabbrev' => 86400,
@@ -127,6 +120,7 @@ class MagicWord {
'currenttime' => 3600,
'currenthour' => 3600,
'localmonth' => 86400,
+ 'localmonth1' => 86400,
'localmonthname' => 86400,
'localmonthnamegen' => 86400,
'localmonthabbrev' => 86400,
@@ -167,8 +161,14 @@ class MagicWord {
'index',
'noindex',
'staticredirect',
+ 'notitleconvert',
+ 'nocontentconvert',
);
+ static public $mSubstIDs = array(
+ 'subst',
+ 'safesubst',
+ );
static public $mObjects = array();
static public $mDoubleUnderscoreArray = null;
@@ -192,7 +192,7 @@ class MagicWord {
*/
static function &get( $id ) {
wfProfileIn( __METHOD__ );
- if (!array_key_exists( $id, self::$mObjects ) ) {
+ if ( !isset( self::$mObjects[$id] ) ) {
$mw = new MagicWord();
$mw->load( $id );
self::$mObjects[$id] = $mw;
@@ -220,6 +220,13 @@ class MagicWord {
return self::$mVariableIDs;
}
+ /**
+ * Get an array of parser substitution modifier IDs
+ */
+ static function getSubstIDs() {
+ return self::$mSubstIDs;
+ }
+
/* Allow external reads of TTL array */
static function getCacheTTL($id) {
if (array_key_exists($id,self::$mCacheTTLs)) {
@@ -237,6 +244,14 @@ class MagicWord {
return self::$mDoubleUnderscoreArray;
}
+ /**
+ * Clear the self::$mObjects variable
+ * For use in parser tests
+ */
+ public static function clearCache() {
+ self::$mObjects = array();
+ }
+
# Initialises this object with an ID
function load( $id ) {
global $wgContLang;
@@ -320,7 +335,7 @@ class MagicWord {
* @return bool
*/
function match( $text ) {
- return preg_match( $this->getRegex(), $text );
+ return (bool)preg_match( $this->getRegex(), $text );
}
/**
@@ -328,7 +343,7 @@ class MagicWord {
* @return bool
*/
function matchStart( $text ) {
- return preg_match( $this->getRegexStart(), $text );
+ return (bool)preg_match( $this->getRegexStart(), $text );
}
/**
@@ -341,7 +356,7 @@ class MagicWord {
$matches = array();
$matchcount = preg_match( $this->getVariableStartToEndRegex(), $text, $matches );
if ( $matchcount == 0 ) {
- return NULL;
+ return null;
} else {
# multiple matched parts (variable match); some will be empty because of
# synonyms. The variable will be the second non-empty one so remove any
@@ -555,7 +570,7 @@ class MagicWordArray {
}
/**
- * Get an unanchored regex
+ * Get an unanchored regex that does not match parameters
*/
function getRegex() {
if ( is_null( $this->regex ) ) {
@@ -572,14 +587,29 @@ class MagicWordArray {
}
/**
- * Get a regex for matching variables
+ * Get a regex for matching variables with parameters
*/
function getVariableRegex() {
return str_replace( "\\$1", "(.*?)", $this->getRegex() );
}
/**
- * Get an anchored regex for matching variables
+ * Get a regex anchored to the start of the string that does not match parameters
+ */
+ function getRegexStart() {
+ $base = $this->getBaseRegex();
+ $newRegex = array( '', '' );
+ if ( $base[0] !== '' ) {
+ $newRegex[0] = "/^(?:{$base[0]})/iuS";
+ }
+ if ( $base[1] !== '' ) {
+ $newRegex[1] = "/^(?:{$base[1]})/S";
+ }
+ return $newRegex;
+ }
+
+ /**
+ * Get an anchored regex for matching variables with parameters
*/
function getVariableStartToEndRegex() {
$base = $this->getBaseRegex();
@@ -676,4 +706,29 @@ class MagicWordArray {
}
return $found;
}
+
+ /**
+ * Return the ID of the magic word at the start of $text, and remove
+ * the prefix from $text.
+ * Return false if no match found and $text is not modified.
+ * Does not match parameters.
+ */
+ public function matchStartAndRemove( &$text ) {
+ $regexes = $this->getRegexStart();
+ foreach ( $regexes as $regex ) {
+ if ( $regex === '' ) {
+ continue;
+ }
+ if ( preg_match( $regex, $text, $m ) ) {
+ list( $id, $param ) = $this->parseMatch( $m );
+ if ( strlen( $m[0] ) >= strlen( $text ) ) {
+ $text = '';
+ } else {
+ $text = substr( $text, strlen( $m[0] ) );
+ }
+ return $id;
+ }
+ }
+ return false;
+ }
}
diff --git a/includes/Math.php b/includes/Math.php
index 2ed16033..8cf9b8d8 100644
--- a/includes/Math.php
+++ b/includes/Math.php
@@ -33,8 +33,7 @@ class MathRenderer {
function render() {
global $wgTmpDirectory, $wgInputEncoding;
- global $wgTexvc;
- $fname = 'MathRenderer::render';
+ global $wgTexvc, $wgMathCheckFiles, $wgTexvcBackgroundColor;
if( $this->mode == MW_MATH_SOURCE ) {
# No need to render or parse anything more!
@@ -45,13 +44,15 @@ class MathRenderer {
}
if( !$this->_recall() ) {
- # Ensure that the temp and output directories are available before continuing...
- if( !file_exists( $wgTmpDirectory ) ) {
- if( !wfMkdirParents( $wgTmpDirectory ) ) {
+ if( $wgMathCheckFiles ) {
+ # Ensure that the temp and output directories are available before continuing...
+ if( !file_exists( $wgTmpDirectory ) ) {
+ if( !wfMkdirParents( $wgTmpDirectory ) ) {
+ return $this->_error( 'math_bad_tmpdir' );
+ }
+ } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) {
return $this->_error( 'math_bad_tmpdir' );
}
- } elseif( !is_dir( $wgTmpDirectory ) || !is_writable( $wgTmpDirectory ) ) {
- return $this->_error( 'math_bad_tmpdir' );
}
if( function_exists( 'is_executable' ) && !is_executable( $wgTexvc ) ) {
@@ -61,7 +62,8 @@ class MathRenderer {
escapeshellarg( $wgTmpDirectory ).' '.
escapeshellarg( $wgTmpDirectory ).' '.
escapeshellarg( $this->tex ).' '.
- escapeshellarg( $wgInputEncoding );
+ escapeshellarg( $wgInputEncoding ).' '.
+ escapeshellarg( $wgTexvcBackgroundColor );
if ( wfIsWindows() ) {
# Invoke it within cygwin sh, because texvc expects sh features in its default shell
@@ -101,14 +103,14 @@ class MathRenderer {
} else {
$this->conservativeness = 0;
}
- $this->mathml = NULL;
+ $this->mathml = null;
} else if ($retval == 'X') {
- $this->html = NULL;
+ $this->html = null;
$this->mathml = substr ($contents, 33);
$this->conservativeness = 0;
} else if ($retval == '+') {
- $this->html = NULL;
- $this->mathml = NULL;
+ $this->html = null;
+ $this->mathml = null;
$this->conservativeness = 0;
} else {
$errbit = htmlspecialchars( substr($contents, 1) );
@@ -176,7 +178,7 @@ class MathRenderer {
'math_html_conservativeness' => $this->conservativeness,
'math_html' => $this->html,
'math_mathml' => $this->mathml,
- ), $fname
+ ), __METHOD__
);
}
@@ -200,15 +202,14 @@ class MathRenderer {
}
function _recall() {
- global $wgMathDirectory;
- $fname = 'MathRenderer::_recall';
+ global $wgMathDirectory, $wgMathCheckFiles;
$this->md5 = md5( $this->tex );
$dbr = wfGetDB( DB_SLAVE );
$rpage = $dbr->selectRow( 'math',
array( 'math_outputhash','math_html_conservativeness','math_html','math_mathml' ),
array( 'math_inputhash' => $dbr->encodeBlob(pack("H32", $this->md5))), # Binary packed, not hex
- $fname
+ __METHOD__
);
if( $rpage !== false ) {
@@ -221,6 +222,12 @@ class MathRenderer {
$this->mathml = $rpage->math_mathml;
$filename = $this->_getHashPath() . "/{$this->hash}.png";
+
+ if( !$wgMathCheckFiles ) {
+ // Short-circuit the file existence & migration checks
+ return true;
+ }
+
if( file_exists( $filename ) ) {
if( filesize( $filename ) == 0 ) {
// Some horrible error corrupted stuff :(
diff --git a/includes/MediaTransformOutput.php b/includes/MediaTransformOutput.php
index 0367494f..a3fcc96e 100644
--- a/includes/MediaTransformOutput.php
+++ b/includes/MediaTransformOutput.php
@@ -80,20 +80,19 @@ abstract class MediaTransformOutput {
}
}
- function getDescLinkAttribs( $alt = false, $params = '' ) {
+ function getDescLinkAttribs( $title = null, $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(
+ $attribs = array(
'href' => $this->file->getTitle()->getLocalURL( $query ),
'class' => 'image',
- 'title' => $alt
);
+ if ( $title ) {
+ $attribs['title'] = $title;
+ }
+ return $attribs;
}
}
@@ -151,18 +150,22 @@ class ThumbnailImage extends MediaTransformOutput {
}
$alt = empty( $options['alt'] ) ? '' : $options['alt'];
- # Note: if title is empty and alt is not, make the title empty, don't
- # use alt; only use alt if title is not set
- $title = !isset( $options['title'] ) ? $alt : $options['title'];
- $query = empty($options['desc-query']) ? '' : $options['desc-query'];
+
+ $query = empty( $options['desc-query'] ) ? '' : $options['desc-query'];
if ( !empty( $options['custom-url-link'] ) ) {
$linkAttribs = array( 'href' => $options['custom-url-link'] );
+ if ( !empty( $options['title'] ) ) {
+ $linkAttribs['title'] = $options['title'];
+ }
} elseif ( !empty( $options['custom-title-link'] ) ) {
$title = $options['custom-title-link'];
- $linkAttribs = array( 'href' => $title->getLinkUrl(), 'title' => $title->getFullText() );
+ $linkAttribs = array(
+ 'href' => $title->getLinkUrl(),
+ 'title' => empty( $options['title'] ) ? $title->getFullText() : $options['title']
+ );
} elseif ( !empty( $options['desc-link'] ) ) {
- $linkAttribs = $this->getDescLinkAttribs( $title, $query );
+ $linkAttribs = $this->getDescLinkAttribs( empty( $options['title'] ) ? null : $options['title'], $query );
} elseif ( !empty( $options['file-link'] ) ) {
$linkAttribs = array( 'href' => $this->file->getURL() );
} else {
@@ -174,7 +177,6 @@ class ThumbnailImage extends MediaTransformOutput {
'src' => $this->url,
'width' => $this->width,
'height' => $this->height,
- 'border' => 0,
);
if ( !empty( $options['valign'] ) ) {
$attribs['style'] = "vertical-align: {$options['valign']}";
diff --git a/includes/MessageCache.php b/includes/MessageCache.php
index 2236bdd7..2c53430f 100644
--- a/includes/MessageCache.php
+++ b/includes/MessageCache.php
@@ -23,9 +23,6 @@ class MessageCache {
var $mUseCache, $mDisable, $mExpiry;
var $mKeys, $mParserOptions, $mParser;
- var $mExtensionMessages = array();
- var $mInitialised = false;
- var $mAllMessagesLoaded = array(); // Extension messages
// Variable for tracking which variables are loaded
var $mLoadedLanguages = array();
@@ -37,7 +34,6 @@ class MessageCache {
$this->mExpiry = $expiry;
$this->mDisableTransform = false;
$this->mKeys = false; # initialised on demand
- $this->mInitialised = true;
$this->mParser = null;
}
@@ -62,9 +58,9 @@ class MessageCache {
* @return false on failure.
*/
function loadFromLocal( $hash, $code ) {
- global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
+ global $wgCacheDirectory, $wgLocalMessageCacheSerialized;
- $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
+ $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
# Check file existence
wfSuppressWarnings();
@@ -106,10 +102,10 @@ class MessageCache {
* Save the cache to a local file.
*/
function saveToLocal( $serialized, $hash, $code ) {
- global $wgLocalMessageCache;
+ global $wgCacheDirectory;
- $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
- wfMkdirParents( $wgLocalMessageCache ); // might fail
+ $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
+ wfMkdirParents( $wgCacheDirectory ); // might fail
wfSuppressWarnings();
$file = fopen( $filename, 'w' );
@@ -126,11 +122,11 @@ class MessageCache {
}
function saveToScript( $array, $hash, $code ) {
- global $wgLocalMessageCache;
+ global $wgCacheDirectory;
- $filename = "$wgLocalMessageCache/messages-" . wfWikiID() . "-$code";
+ $filename = "$wgCacheDirectory/messages-" . wfWikiID() . "-$code";
$tempFilename = $filename . '.tmp';
- wfMkdirParents( $wgLocalMessageCache ); // might fail
+ wfMkdirParents( $wgCacheDirectory ); // might fail
wfSuppressWarnings();
$file = fopen( $tempFilename, 'w');
@@ -174,7 +170,7 @@ class MessageCache {
/**
* Loads messages from caches or from database in this order:
- * (1) local message cache (if $wgLocalMessageCache is enabled)
+ * (1) local message cache (if $wgUseLocalMessageCache is enabled)
* (2) memcached
* (3) from the database.
*
@@ -191,7 +187,7 @@ class MessageCache {
* @param $code String: language to which load messages
*/
function load( $code = false ) {
- global $wgLocalMessageCache;
+ global $wgUseLocalMessageCache;
if ( !$this->mUseCache ) {
return true;
@@ -227,7 +223,7 @@ class MessageCache {
# (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 ) {
+ if ( $wgUseLocalMessageCache ) {
wfProfileIn( __METHOD__ . '-fromlocal' );
$hash = $this->mMemc->get( wfMemcKey( 'messages', $code, 'hash' ) );
@@ -260,7 +256,7 @@ class MessageCache {
$this->lock($cacheKey);
- # Limit the concurrency of loadFromDB to a single process
+ # Limit the concurrency of loadFromDB to a single process
# This prevents the site from going down when the cache expires
$statusKey = wfMemcKey( 'messages', $code, 'status' );
$success = $this->mMemc->add( $statusKey, 'loading', MSG_LOAD_TIMEOUT );
@@ -322,12 +318,11 @@ class MessageCache {
# database or in code.
if ( $code !== $wgContLanguageCode ) {
# Messages for particular language
- $escapedCode = $dbr->escapeLike( $code );
- $conds[] = "page_title like '%%/$escapedCode'";
+ $conds[] = 'page_title' . $dbr->buildLike( $dbr->anyString(), "/$code" );
} else {
# Effectively disallows use of '/' character in NS_MEDIAWIKI for uses
# other than language code.
- $conds[] = "page_title not like '%%/%%'";
+ $conds[] = 'page_title NOT' . $dbr->buildLike( $dbr->anyString(), '/', $dbr->anyString() );
}
}
@@ -350,7 +345,7 @@ class MessageCache {
$res = $dbr->select( array( 'page', 'revision', 'text' ),
array( 'page_title', 'old_text', 'old_flags' ),
- $smallConds, __METHOD__ );
+ $smallConds, __METHOD__. "($code)" );
for ( $row = $dbr->fetchObject( $res ); $row; $row = $dbr->fetchObject( $res ) ) {
$cache[$row->page_title] = ' ' . Revision::getRevisionText( $row );
@@ -404,8 +399,19 @@ class MessageCache {
// Also delete cached sidebar... just in case it is affected
global $parserMemc;
- $sidebarKey = wfMemcKey( 'sidebar', $code );
- $parserMemc->delete( $sidebarKey );
+ $codes = array( $code );
+ if ( $code === 'en' ) {
+ // Delete all sidebars, like for example on action=purge on the
+ // sidebar messages
+ $codes = array_keys( Language::getLanguageNames() );
+ }
+
+ foreach ( $codes as $code ) {
+ $sidebarKey = wfMemcKey( 'sidebar', $code );
+ $parserMemc->delete( $sidebarKey );
+ }
+
+ wfRunHooks( "MessageCacheReplace", array( $title, $text ) );
wfProfileOut( __METHOD__ );
}
@@ -421,24 +427,18 @@ class MessageCache {
*/
protected function saveToCaches( $cache, $memc = true, $code = false ) {
wfProfileIn( __METHOD__ );
- global $wgLocalMessageCache, $wgLocalMessageCacheSerialized;
+ global $wgUseLocalMessageCache, $wgLocalMessageCacheSerialized;
$cacheKey = wfMemcKey( 'messages', $code );
- $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));
- }
+ $success = $this->mMemc->set( $cacheKey, $cache, $this->mExpiry );
+ } else {
+ $success = true;
}
# Save to local cache
- if ( $wgLocalMessageCache !== false ) {
+ if ( $wgUseLocalMessageCache ) {
$serialized = serialize( $cache );
$hash = md5( $serialized );
$this->mMemc->set( wfMemcKey( 'messages', $code, 'hash' ), $hash, $this->mExpiry );
@@ -449,11 +449,6 @@ class MessageCache {
}
}
- if ( $i == 20 ) {
- $success = false;
- } else {
- $success = true;
- }
wfProfileOut( __METHOD__ );
return $success;
}
@@ -498,42 +493,40 @@ class MessageCache {
* 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".
+ * @param bool $isFullKey Specifies whether $key is a two part key "msg/lang".
*/
function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) {
global $wgContLanguageCode, $wgContLang;
+ if ( strval( $key ) === '' ) {
+ # Shortcut: the empty key is always missing
+ return '&lt;&gt;';
+ }
+
$lang = wfGetLangObj( $langcode );
$langcode = $lang->getCode();
- # If uninitialised, someone is trying to call this halfway through Setup.php
- if( !$this->mInitialised ) {
- return '&lt;' . htmlspecialchars($key) . '&gt;';
- }
-
$message = false;
- # Normalise title-case input
- $lckey = $wgContLang->lcfirst( $key );
- $lckey = str_replace( ' ', '_', $lckey );
+ # Normalise title-case input (with some inlining)
+ $lckey = str_replace( ' ', '_', $key );
+ if ( ord( $key ) < 128 ) {
+ $lckey[0] = strtolower( $lckey[0] );
+ $uckey = ucfirst( $lckey );
+ } else {
+ $lckey = $wgContLang->lcfirst( $lckey );
+ $uckey = $wgContLang->ucfirst( $lckey );
+ }
# Try the MediaWiki namespace
if( !$this->mDisable && $useDB ) {
- $title = $wgContLang->ucfirst( $lckey );
- if(!$isFullKey && ($langcode != $wgContLanguageCode) ) {
+ $title = $uckey;
+ if(!$isFullKey && ( $langcode != $wgContLanguageCode ) ) {
$title .= '/' . $langcode;
}
$message = $this->getMsgFromNamespace( $title, $langcode );
}
- # Try the extension array
- if ( $message === false && isset( $this->mExtensionMessages[$langcode][$lckey] ) ) {
- $message = $this->mExtensionMessages[$langcode][$lckey];
- }
- if ( $message === false && isset( $this->mExtensionMessages['en'][$lckey] ) ) {
- $message = $this->mExtensionMessages['en'][$lckey];
- }
-
# Try the array in the language object
if ( $message === false ) {
$message = $lang->getMessage( $lckey );
@@ -543,19 +536,15 @@ class MessageCache {
}
# Try the array of another language
- $pos = strrpos( $lckey, '/' );
- if( $message === false && $pos !== false) {
- $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;
- }
+ if( $message === false ) {
+ $parts = explode( '/', $lckey );
+ # We may get calls for things that are http-urls from sidebar
+ # Let's not load nonexistent languages for those
+ # They usually have more than one slash.
+ if ( count( $parts ) == 2 && $parts[1] !== '' ) {
+ $message = Language::getMessageFor( $parts[0], $parts[1] );
+ if ( is_null( $message ) ) {
+ $message = false;
}
}
}
@@ -564,13 +553,23 @@ class MessageCache {
if( ($message === false || $message === '-' ) &&
!$this->mDisable && $useDB &&
!$isFullKey && ($langcode != $wgContLanguageCode) ) {
- $message = $this->getMsgFromNamespace( $wgContLang->ucfirst( $lckey ), $wgContLanguageCode );
+ $message = $this->getMsgFromNamespace( $uckey, $wgContLanguageCode );
}
# Final fallback
if( $message === false ) {
return '&lt;' . htmlspecialchars($key) . '&gt;';
}
+
+ # Fix whitespace
+ $message = strtr( $message,
+ array(
+ # Fix for trailing whitespace, removed by textarea
+ '&#32;' => ' ',
+ # Fix for NBSP, converted to space by firefox
+ '&nbsp;' => "\xc2\xa0",
+ ) );
+
return $message;
}
@@ -636,7 +635,7 @@ class MessageCache {
$message = $revision->getText();
if ($this->mUseCache) {
$this->mCache[$code][$title] = ' ' . $message;
- $this->mMemc->set( $titleKey, $message, $this->mExpiry );
+ $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
}
} else {
# Negative caching
@@ -648,7 +647,7 @@ class MessageCache {
}
function transform( $message, $interface = false, $language = null ) {
- // Avoid creating parser if nothing to transfrom
+ // Avoid creating parser if nothing to transform
if( strpos( $message, '{{' ) === false ) {
return $message;
}
@@ -678,7 +677,7 @@ class MessageCache {
function disable() { $this->mDisable = true; }
function enable() { $this->mDisable = false; }
-
+
/** @deprecated */
function disableTransform(){
wfDeprecated( __METHOD__ );
@@ -695,160 +694,76 @@ class MessageCache {
}
/**
+ * Clear all stored messages. Mainly used after a mass rebuild.
+ */
+ function clear() {
+ if( $this->mUseCache ) {
+ $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' ) );
+ }
+ }
+ }
+
+ /**
* Add a message to the cache
+ * @deprecated Use $wgExtensionMessagesFiles
*
* @param mixed $key
* @param mixed $value
* @param string $lang The messages language, English by default
*/
function addMessage( $key, $value, $lang = 'en' ) {
- global $wgContLang;
- # Normalise title-case input
- $lckey = str_replace( ' ', '_', $wgContLang->lcfirst( $key ) );
- $this->mExtensionMessages[$lang][$lckey] = $value;
+ wfDeprecated( __METHOD__ );
+ $lc = Language::getLocalisationCache();
+ $lc->addLegacyMessages( array( $lang => array( $key => $value ) ) );
}
/**
* Add an associative array of message to the cache
+ * @deprecated Use $wgExtensionMessagesFiles
*
* @param array $messages An associative array of key => values to be added
* @param string $lang The messages language, English by default
*/
function addMessages( $messages, $lang = 'en' ) {
- wfProfileIn( __METHOD__ );
- if ( !is_array( $messages ) ) {
- throw new MWException( __METHOD__.': Invalid message array' );
- }
- if ( isset( $this->mExtensionMessages[$lang] ) ) {
- $this->mExtensionMessages[$lang] = $messages + $this->mExtensionMessages[$lang];
- } else {
- $this->mExtensionMessages[$lang] = $messages;
- }
- wfProfileOut( __METHOD__ );
+ wfDeprecated( __METHOD__ );
+ $lc = Language::getLocalisationCache();
+ $lc->addLegacyMessages( array( $lang => $messages ) );
}
/**
* Add a 2-D array of messages by lang. Useful for extensions.
+ * @deprecated Use $wgExtensionMessagesFiles
*
* @param array $messages The array to be added
*/
function addMessagesByLang( $messages ) {
- wfProfileIn( __METHOD__ );
- foreach ( $messages as $key => $value ) {
- $this->addMessages( $value, $key );
- }
- wfProfileOut( __METHOD__ );
+ wfDeprecated( __METHOD__ );
+ $lc = Language::getLocalisationCache();
+ $lc->addLegacyMessages( $messages );
}
/**
- * Get the extension messages for a specific language. Only English, interface
- * and content language are guaranteed to be loaded.
- *
- * @param string $lang The messages language, English by default
+ * Set a hook for addMessagesByLang()
*/
- function getExtensionMessagesFor( $lang = 'en' ) {
- wfProfileIn( __METHOD__ );
- $messages = array();
- if ( isset( $this->mExtensionMessages[$lang] ) ) {
- $messages = $this->mExtensionMessages[$lang];
- }
- if ( $lang != 'en' ) {
- $messages = $messages + $this->mExtensionMessages['en'];
- }
- wfProfileOut( __METHOD__ );
- return $messages;
+ function setExtensionMessagesHook( $callback ) {
+ $this->mAddMessagesHook = $callback;
}
/**
- * Clear all stored messages. Mainly used after a mass rebuild.
+ * @deprecated
*/
- function clear() {
- if( $this->mUseCache ) {
- $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' ) );
- }
- }
- }
-
function loadAllMessages( $lang = false ) {
- global $wgExtensionMessagesFiles;
- $key = $lang === false ? '*' : $lang;
- if ( isset( $this->mAllMessagesLoaded[$key] ) ) {
- return;
- }
- $this->mAllMessagesLoaded[$key] = true;
-
- # Some extensions will load their messages when you load their class file
- wfLoadAllExtensions();
- # Others will respond to this hook
- wfRunHooks( 'LoadAllMessages' );
- # Some register their messages in $wgExtensionMessagesFiles
- foreach ( $wgExtensionMessagesFiles as $name => $file ) {
- wfLoadExtensionMessages( $name, $lang );
- }
- # 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).
+ * @deprecated
*/
function loadMessagesFile( $filename, $langcode = false ) {
- global $wgLang, $wgContLang;
- wfProfileIn( __METHOD__ );
- $messages = $magicWords = false;
- require( $filename );
-
- $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 ) {
- global $wgContLang;
- $wgContLang->addMagicWordsByLang( $magicWords );
- }
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * 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 ) {
- wfProfileIn( __METHOD__ );
- $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 );
- wfProfileOut( __METHOD__ );
}
public function figureMessage( $key ) {
diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php
index d52de994..39c82c9d 100644
--- a/includes/MimeMagic.php
+++ b/includes/MimeMagic.php
@@ -118,19 +118,19 @@ class MimeMagic {
* Mapping of media types to arrays of mime types.
* This is used by findMediaType and getMediaType, respectively
*/
- var $mMediaTypes= NULL;
+ var $mMediaTypes= null;
/** Map of mime type aliases
*/
- var $mMimeTypeAliases= NULL;
+ var $mMimeTypeAliases= null;
/** map of mime types to file extensions (as a space seprarated list)
*/
- var $mMimeToExt= NULL;
+ var $mMimeToExt= null;
/** map of file extensions types to mime types (as a space seprarated list)
*/
- var $mExtToMime= NULL;
+ var $mExtToMime= null;
/** IEContentAnalyzer instance
*/
@@ -328,7 +328,7 @@ class MimeMagic {
*/
function guessTypesForExtension( $ext ) {
$m = $this->getTypesForExtension( $ext );
- if ( is_null( $m ) ) return NULL;
+ if ( is_null( $m ) ) return null;
$m = trim( $m );
$m = preg_replace( '/\s.*$/', '', $m );
@@ -345,7 +345,7 @@ class MimeMagic {
$ext = $this->getExtensionsForType( $mime );
if ( !$ext ) {
- return NULL; //unknown
+ return null; //unknown
}
$ext = explode( ' ', $ext );
@@ -469,16 +469,18 @@ class MimeMagic {
}
/*
- * look for PHP
- * Check for this before HTML/XML...
- * Warning: this is a heuristic, and won't match a file with a lot of non-PHP before.
- * It will also match text files which could be PHP. :)
+ * Look for PHP. Check for this before HTML/XML... Warning: this is a
+ * heuristic, and won't match a file with a lot of non-PHP before. It
+ * will also match text files which could be PHP. :)
+ *
+ * FIXME: For this reason, the check is probably useless -- an attacker
+ * could almost certainly just pad the file with a lot of nonsense to
+ * circumvent the check in any case where it would be a security
+ * problem. On the other hand, it causes harmful false positives (bug
+ * 16583). The heuristic has been cut down to exclude three-character
+ * strings like "<? ", but should it be axed completely?
*/
if( ( strpos( $head, '<?php' ) !== false ) ||
- ( strpos( $head, '<? ' ) !== false ) ||
- ( strpos( $head, "<?\n" ) !== false ) ||
- ( strpos( $head, "<?\t" ) !== false ) ||
- ( strpos( $head, "<?=" ) !== false ) ||
( strpos( $head, "<\x00?\x00p\x00h\x00p" ) !== false ) ||
( strpos( $head, "<\x00?\x00 " ) !== false ) ||
@@ -506,7 +508,7 @@ class MimeMagic {
/*
* look for shell scripts
*/
- $script_type = NULL;
+ $script_type = null;
# detect by shebang
if ( substr( $head, 0, 2) == "#!" ) {
@@ -629,7 +631,7 @@ class MimeMagic {
function detectMimeType( $file, $ext = true ) {
global $wgMimeDetectorCommand;
- $m = NULL;
+ $m = null;
if ( $wgMimeDetectorCommand ) {
$fn = wfEscapeShellArg( $file );
$m = `$wgMimeDetectorCommand $fn`;
@@ -676,7 +678,7 @@ class MimeMagic {
$m = strtolower( $m );
if ( strpos( $m, 'unknown' ) !== false ) {
- $m = NULL;
+ $m = null;
} else {
wfDebug( __METHOD__.": magic mime type of $file: $m\n" );
return $m;
@@ -721,7 +723,7 @@ class MimeMagic {
*
* @return (int?string?) a value to be used with the MEDIATYPE_xxx constants.
*/
- function getMediaType( $path = NULL, $mime = NULL ) {
+ function getMediaType( $path = null, $mime = null ) {
if( !$mime && !$path ) return MEDIATYPE_UNKNOWN;
# If mime type is unknown, guess it
@@ -754,7 +756,7 @@ class MimeMagic {
}
# Check for entry for file extension
- $e = NULL;
+ $e = null;
if ( $path ) {
$i = strrpos( $path, '.' );
$e = strtolower( $i ? substr( $path, $i + 1 ) : '' );
diff --git a/includes/Namespace.php b/includes/Namespace.php
index 3d618e64..e8e7523f 100644
--- a/includes/Namespace.php
+++ b/includes/Namespace.php
@@ -28,7 +28,7 @@ $wgCanonicalNamespaceNames = array(
NS_CATEGORY_TALK => 'Category_talk',
);
-if( is_array( $wgExtraNamespaces ) ) {
+if( isset( $wgExtraNamespaces ) && is_array( $wgExtraNamespaces ) ) {
$wgCanonicalNamespaceNames = $wgCanonicalNamespaceNames + $wgExtraNamespaces;
}
@@ -46,6 +46,13 @@ if( is_array( $wgExtraNamespaces ) ) {
class MWNamespace {
/**
+ * These namespaces should always be first-letter capitalized, now and
+ * forevermore. Historically, they could've probably been lowercased too,
+ * but some things are just too ingrained now. :)
+ */
+ private static $alwaysCapitalizedNamespaces = array( NS_SPECIAL, NS_USER, NS_MEDIAWIKI );
+
+ /**
* Can pages in the given namespace be moved?
*
* @param $index Int: namespace index
@@ -102,6 +109,15 @@ class MWNamespace {
}
/**
+ * Returns whether the specified namespace exists
+ */
+ public static function exists( $index ) {
+ global $wgCanonicalNamespaceNames;
+ return isset( $wgCanonicalNamespaceNames[$index] );
+ }
+
+
+ /**
* Returns the canonical (English Wikipedia) name for a given index
*
* @param $index Int: namespace index
@@ -135,7 +151,7 @@ class MWNamespace {
if ( array_key_exists( $name, $xNamespaces ) ) {
return $xNamespaces[$name];
} else {
- return NULL;
+ return null;
}
}
@@ -146,7 +162,7 @@ class MWNamespace {
* @return bool
*/
public static function canTalk( $index ) {
- return $index >= NS_MAIN;
+ return $index >= NS_MAIN;
}
/**
@@ -182,4 +198,29 @@ class MWNamespace {
return !empty( $wgNamespacesWithSubpages[$index] );
}
+ /**
+ * Is the namespace first-letter capitalized?
+ *
+ * @param $index int Index to check
+ * @return bool
+ */
+ public static function isCapitalized( $index ) {
+ global $wgCapitalLinks, $wgCapitalLinkOverrides;
+ // Turn NS_MEDIA into NS_FILE
+ $index = $index === NS_MEDIA ? NS_FILE : $index;
+
+ // Make sure to get the subject of our namespace
+ $index = self::getSubject( $index );
+
+ // Some namespaces are special and should always be upper case
+ if ( in_array( $index, self::$alwaysCapitalizedNamespaces ) ) {
+ return true;
+ }
+ if ( isset( $wgCapitalLinkOverrides[ $index ] ) ) {
+ // $wgCapitalLinkOverrides is explicitly set
+ return $wgCapitalLinkOverrides[ $index ];
+ }
+ // Default to the global setting
+ return $wgCapitalLinks;
+ }
}
diff --git a/includes/ObjectCache.php b/includes/ObjectCache.php
index 6cfb2340..f83e0020 100644
--- a/includes/ObjectCache.php
+++ b/includes/ObjectCache.php
@@ -34,7 +34,7 @@ $wgCaches = array();
/**
* Get a cache object.
- * @param int $inputType cache type, one the the CACHE_* constants.
+ * @param $inputType Integer: cache type, one the the CACHE_* constants.
*/
function &wfGetCache( $inputType ) {
global $wgCaches, $wgMemCachedServers, $wgMemCachedDebug, $wgMemCachedPersistent;
@@ -52,13 +52,6 @@ function &wfGetCache( $inputType ) {
if ( $type == CACHE_MEMCACHED ) {
if ( !array_key_exists( CACHE_MEMCACHED, $wgCaches ) ) {
- if ( !class_exists( 'MemcachedClientforWiki' ) ) {
- class MemCachedClientforWiki extends memcached {
- function _debugprint( $text ) {
- wfDebug( "memcached: $text" );
- }
- }
- }
$wgCaches[CACHE_MEMCACHED] = new MemCachedClientforWiki(
array('persistant' => $wgMemCachedPersistent, 'compress_threshold' => 1500 ) );
$wgCaches[CACHE_MEMCACHED]->set_servers( $wgMemCachedServers );
@@ -73,8 +66,6 @@ function &wfGetCache( $inputType ) {
$wgCaches[CACHE_ACCEL] = new APCBagOStuff;
} elseif( function_exists( 'xcache_get' ) ) {
$wgCaches[CACHE_ACCEL] = new XCacheBagOStuff();
- } elseif ( function_exists( 'mmcache_get' ) ) {
- $wgCaches[CACHE_ACCEL] = new TurckBagOStuff;
} else {
$wgCaches[CACHE_ACCEL] = false;
}
@@ -91,7 +82,7 @@ function &wfGetCache( $inputType ) {
if ( $type == CACHE_DB || ( $inputType == CACHE_ANYTHING && $cache === false ) ) {
if ( !array_key_exists( CACHE_DB, $wgCaches ) ) {
- $wgCaches[CACHE_DB] = new MediaWikiBagOStuff('objectcache');
+ $wgCaches[CACHE_DB] = new SqlBagOStuff('objectcache');
}
$cache =& $wgCaches[CACHE_DB];
}
diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php
index 1f4798b7..91819cc7 100644
--- a/includes/OutputHandler.php
+++ b/includes/OutputHandler.php
@@ -5,8 +5,8 @@
*/
function wfOutputHandler( $s ) {
global $wgDisableOutputCompression, $wgValidateAllHtml;
- $s = wfMangleFlashPolicy( $s );
- if ( $wgValidateAllHtml ) {
+ $s = wfMangleFlashPolicy( $s );
+ if ( $wgValidateAllHtml ) {
$headers = apache_response_headers();
$isHTML = true;
foreach ( $headers as $name => $value ) {
@@ -37,7 +37,7 @@ function wfOutputHandler( $s ) {
* @private
*/
function wfRequestExtension() {
- /// @fixme -- this sort of dupes some code in WebRequest::getRequestUrl()
+ /// @todo Fixme: this sort of dupes some code in WebRequest::getRequestUrl()
if( isset( $_SERVER['REQUEST_URI'] ) ) {
// Strip the query string...
list( $path ) = explode( '?', $_SERVER['REQUEST_URI'], 2 );
@@ -74,12 +74,9 @@ function wfGzipHandler( $s ) {
return $s;
}
- if( isset( $_SERVER['HTTP_ACCEPT_ENCODING'] ) ) {
- $tokens = preg_split( '/[,; ]/', $_SERVER['HTTP_ACCEPT_ENCODING'] );
- if ( in_array( 'gzip', $tokens ) ) {
- header( 'Content-Encoding: gzip' );
- $s = gzencode( $s, 3 );
- }
+ if( wfClientAcceptsGzip() ) {
+ header( 'Content-Encoding: gzip' );
+ $s = gzencode( $s, 6 );
}
// Set vary header if it hasn't been set already
@@ -93,7 +90,10 @@ function wfGzipHandler( $s ) {
}
if ( !$foundVary ) {
header( 'Vary: Accept-Encoding' );
- header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip' );
+ global $wgUseXVO;
+ if ( $wgUseXVO ) {
+ header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip' );
+ }
}
return $s;
}
@@ -133,7 +133,7 @@ function wfHtmlValidationHandler( $s ) {
$out = <<<EOT
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" dir="ltr">
<head>
<title>HTML validation error</title>
<style>
diff --git a/includes/OutputPage.php b/includes/OutputPage.php
index 046a01a8..4333383c 100644
--- a/includes/OutputPage.php
+++ b/includes/OutputPage.php
@@ -9,22 +9,25 @@ class OutputPage {
var $mMetatags = array(), $mKeywords = array(), $mLinktags = array();
var $mExtStyles = array();
var $mPagetitle = '', $mBodytext = '', $mDebugtext = '';
- var $mHTMLtitle = '', $mIsarticle = true, $mPrintable = false;
+ var $mHTMLtitle = '', $mHTMLtitleFromPagetitle = true, $mIsarticle = true, $mPrintable = false;
var $mSubtitle = '', $mRedirect = '', $mStatusCode;
var $mLastModified = '', $mETag = false;
- var $mCategoryLinks = array(), $mLanguageLinks = array();
+ var $mCategoryLinks = array(), $mCategories = array(), $mLanguageLinks = array();
+
var $mScripts = '', $mLinkColours, $mPageLinkTitle = '', $mHeadItems = array();
+ var $mInlineMsg = array();
+
var $mTemplateIds = array();
var $mAllowUserJs;
var $mSuppressQuickbar = false;
- var $mOnloadHandler = '';
var $mDoNothing = false;
var $mContainsOldMagic = 0, $mContainsNewMagic = 0;
var $mIsArticleRelated = true;
protected $mParserOptions = null; // lazy initialised, use parserOptions()
- var $mShowFeedLinks = false;
- var $mFeedLinksAppendQuery = false;
+
+ var $mFeedLinks = array();
+
var $mEnableClientCache = true;
var $mArticleBodyOnly = false;
@@ -35,6 +38,7 @@ class OutputPage {
var $mParseWarnings = array();
var $mSquidMaxage = 0;
var $mRevisionId = null;
+ protected $mTitle = null;
/**
* An array of stylesheet filenames (relative from skins path), with options
@@ -43,8 +47,16 @@ class OutputPage {
*/
var $styles = array();
+ /**
+ * Whether to load jQuery core.
+ */
+ protected $mJQueryDone = false;
+
private $mIndexPolicy = 'index';
private $mFollowPolicy = 'follow';
+ private $mVaryHeader = array( 'Accept-Encoding' => array('list-contains=gzip'),
+ 'Cookie' => null );
+
/**
* Constructor
@@ -55,12 +67,23 @@ class OutputPage {
$this->mAllowUserJs = $wgAllowUserJs;
}
+ /**
+ * Redirect to $url rather than displaying the normal page
+ *
+ * @param $url String: URL
+ * @param $responsecode String: HTTP status code
+ */
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;
}
+ /**
+ * Get the URL to redirect to, or an empty string if not redirect URL set
+ *
+ * @return String
+ */
public function getRedirect() {
return $this->mRedirect;
}
@@ -68,10 +91,13 @@ class OutputPage {
/**
* Set the HTTP status code to send with the output.
*
- * @param int $statusCode
+ * @param $statusCode Integer
* @return nothing
*/
- function setStatusCode( $statusCode ) { $this->mStatusCode = $statusCode; }
+ public function setStatusCode( $statusCode ) {
+ $this->mStatusCode = $statusCode;
+ }
+
/**
* Add a new <meta> tag
@@ -84,41 +110,113 @@ class OutputPage {
array_push( $this->mMetatags, array( $name, $val ) );
}
- function addKeyword( $text ) { array_push( $this->mKeywords, $text ); }
- function addScript( $script ) { $this->mScripts .= "\t\t".$script; }
-
- function addExtensionStyle( $url ) {
- $linkarr = array( 'rel' => 'stylesheet', 'href' => $url, 'type' => 'text/css' );
- array_push( $this->mExtStyles, $linkarr );
+ /**
+ * Add a keyword or a list of keywords in the page header
+ *
+ * @param $text String or array of strings
+ */
+ function addKeyword( $text ) {
+ if( is_array( $text ) ) {
+ $this->mKeywords = array_merge( $this->mKeywords, $text );
+ } else {
+ array_push( $this->mKeywords, $text );
+ }
+ }
+
+ /**
+ * Add a new \<link\> tag to the page header
+ *
+ * @param $linkarr Array: associative array of attributes.
+ */
+ function addLink( $linkarr ) {
+ array_push( $this->mLinktags, $linkarr );
+ }
+
+ /**
+ * Add a new \<link\> with "rel" attribute set to "meta"
+ *
+ * @param $linkarr Array: associative array mapping attribute names to their
+ * values, both keys and values will be escaped, and the
+ * "rel" attribute will be automatically added
+ */
+ function addMetadataLink( $linkarr ) {
+ # note: buggy CC software only reads first "meta" link
+ static $haveMeta = false;
+ $linkarr['rel'] = $haveMeta ? 'alternate meta' : 'meta';
+ $this->addLink( $linkarr );
+ $haveMeta = true;
+ }
+
+
+ /**
+ * Add raw HTML to the list of scripts (including \<script\> tag, etc.)
+ *
+ * @param $script String: raw HTML
+ */
+ function addScript( $script ) {
+ $this->mScripts .= $script . "\n";
+ }
+
+ /**
+ * Register and add a stylesheet from an extension directory.
+ *
+ * @param $url String path to sheet. Provide either a full url (beginning
+ * with 'http', etc) or a relative path from the document root
+ * (beginning with '/'). Otherwise it behaves identically to
+ * addStyle() and draws from the /skins folder.
+ */
+ public function addExtensionStyle( $url ) {
+ array_push( $this->mExtStyles, $url );
+ }
+
+ /**
+ * Get all links added by extensions
+ *
+ * @return Array
+ */
+ function getExtStyle() {
+ return $this->mExtStyles;
}
/**
* 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)
+ *
+ * @param $file String: filename in skins/common or complete on-server path
+ * (/foo/bar.js)
*/
- function addScriptFile( $file ) {
- global $wgStylePath, $wgStyleVersion, $wgJsMimeType;
- if( substr( $file, 0, 1 ) == '/' ) {
+ public function addScriptFile( $file ) {
+ global $wgStylePath, $wgStyleVersion;
+ if( substr( $file, 0, 1 ) == '/' || substr( $file, 0, 7 ) == 'http://' ) {
$path = $file;
} else {
$path = "{$wgStylePath}/common/{$file}";
}
- $this->addScript( "<script type=\"{$wgJsMimeType}\" src=\"$path?$wgStyleVersion\"></script>\n" );
+ $this->addScript( Html::linkedScript( wfAppendQuery( $path, $wgStyleVersion ) ) );
}
-
+
/**
* Add a self-contained script tag with the given contents
- * @param string $script JavaScript text, no <script> tags
+ *
+ * @param $script String: JavaScript text, no <script> tags
*/
- function addInlineScript( $script ) {
- global $wgJsMimeType;
- $this->mScripts .= "<script type=\"$wgJsMimeType\">/*<![CDATA[*/\n$script\n/*]]>*/</script>";
+ public function addInlineScript( $script ) {
+ $this->mScripts .= Html::inlineScript( "\n$script\n" ) . "\n";
}
+ /**
+ * Get all registered JS and CSS tags for the header.
+ *
+ * @return String
+ */
function getScript() {
return $this->mScripts . $this->getHeadItems();
}
+ /**
+ * Get all header items in a string
+ *
+ * @return String
+ */
function getHeadItems() {
$s = '';
foreach ( $this->mHeadItems as $item ) {
@@ -127,36 +225,56 @@ class OutputPage {
return $s;
}
- function addHeadItem( $name, $value ) {
+ /**
+ * Add or replace an header item to the output
+ *
+ * @param $name String: item name
+ * @param $value String: raw HTML
+ */
+ public function addHeadItem( $name, $value ) {
$this->mHeadItems[$name] = $value;
}
- function hasHeadItem( $name ) {
+ /**
+ * Check if the header item $name is already set
+ *
+ * @param $name String: item name
+ * @return Boolean
+ */
+ public function hasHeadItem( $name ) {
return isset( $this->mHeadItems[$name] );
}
- function setETag($tag) { $this->mETag = $tag; }
- function setArticleBodyOnly($only) { $this->mArticleBodyOnly = $only; }
- function getArticleBodyOnly($only) { return $this->mArticleBodyOnly; }
-
- function addLink( $linkarr ) {
- # $linkarr should be an associative array of attributes. We'll escape on output.
- array_push( $this->mLinktags, $linkarr );
+ /**
+ * Set the value of the ETag HTTP header, only used if $wgUseETag is true
+ *
+ * @param $tag String: value of "ETag" header
+ */
+ function setETag( $tag ) {
+ $this->mETag = $tag;
}
-
- # Get all links added by extensions
- function getExtStyle() {
- return $this->mExtStyles;
+
+ /**
+ * Set whether the output should only contain the body of the article,
+ * without any skin, sidebar, etc.
+ * Used e.g. when calling with "action=render".
+ *
+ * @param $only Boolean: whether to output only the body of the article
+ */
+ public function setArticleBodyOnly( $only ) {
+ $this->mArticleBodyOnly = $only;
}
- function addMetadataLink( $linkarr ) {
- # note: buggy CC software only reads first "meta" link
- static $haveMeta = false;
- $linkarr['rel'] = ($haveMeta) ? 'alternate meta' : 'meta';
- $this->addLink( $linkarr );
- $haveMeta = true;
+ /**
+ * Return whether the output will contain only the body of the article
+ *
+ * @return Boolean
+ */
+ public function getArticleBodyOnly() {
+ return $this->mArticleBodyOnly;
}
+
/**
* checkLastModified tells the client to use the client-cached page if
* possible. If sucessful, the OutputPage is disabled so that
@@ -164,11 +282,11 @@ class OutputPage {
*
* Side effect: sets mLastModified for Last-Modified header
*
- * @return bool True iff cache-ok headers was sent.
+ * @return Boolean: true iff cache-ok headers was sent.
*/
- function checkLastModified( $timestamp ) {
+ public function checkLastModified( $timestamp ) {
global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest;
-
+
if ( !$timestamp || $timestamp == '19700101000000' ) {
wfDebug( __METHOD__ . ": CACHE DISABLED, NO TIMESTAMP\n" );
return false;
@@ -221,9 +339,9 @@ class OutputPage {
}
$clientHeaderTime = wfTimestamp( TS_MW, $clientHeaderTime );
- wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
+ wfDebug( __METHOD__ . ": client sent If-Modified-Since: " .
wfTimestamp( TS_ISO_8601, $clientHeaderTime ) . "\n", false );
- wfDebug( __METHOD__ . ": effective Last-Modified: " .
+ wfDebug( __METHOD__ . ": effective Last-Modified: " .
wfTimestamp( TS_ISO_8601, $maxModified ) . "\n", false );
if( $clientHeaderTime < $maxModified ) {
wfDebug( __METHOD__ . ": STALE, $info\n", false );
@@ -231,7 +349,7 @@ class OutputPage {
}
# Not modified
- # Give a 304 response code and disable body output
+ # Give a 304 response code and disable body output
wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", false );
ini_set('zlib.output_compression', 0);
$wgRequest->response()->header( "HTTP/1.1 304 Not Modified" );
@@ -246,40 +364,23 @@ class OutputPage {
return true;
}
- function setPageTitleActionText( $text ) {
- $this->mPageTitleActionText = $text;
- }
-
- function getPageTitleActionText () {
- if ( isset( $this->mPageTitleActionText ) ) {
- return $this->mPageTitleActionText;
- }
- }
/**
* Set the robot policy for the page: <http://www.robotstxt.org/meta.html>
*
- * @param $policy string The literal string to output as the contents of
+ * @param $policy String: the literal string to output as the contents of
* the meta tag. Will be parsed according to the spec and output in
* standardized form.
* @return null
*/
public function setRobotPolicy( $policy ) {
- $policy = explode( ',', $policy );
- $policy = array_map( 'trim', $policy );
+ $policy = Article::formatRobotPolicy( $policy );
- # The default policy is follow, so if nothing is said explicitly, we
- # do that.
- if( in_array( 'nofollow', $policy ) ) {
- $this->mFollowPolicy = 'nofollow';
- } else {
- $this->mFollowPolicy = 'follow';
+ if( isset( $policy['index'] ) ){
+ $this->setIndexPolicy( $policy['index'] );
}
-
- if( in_array( 'noindex', $policy ) ) {
- $this->mIndexPolicy = 'noindex';
- } else {
- $this->mIndexPolicy = 'index';
+ if( isset( $policy['follow'] ) ){
+ $this->setFollowPolicy( $policy['follow'] );
}
}
@@ -301,7 +402,7 @@ class OutputPage {
* Set the follow policy for the page, but leave the index policy un-
* touched.
*
- * @param $policy string Either 'follow' or 'nofollow'.
+ * @param $policy String: either 'follow' or 'nofollow'.
* @return null
*/
public function setFollowPolicy( $policy ) {
@@ -311,43 +412,270 @@ class OutputPage {
}
}
- public function setHTMLTitle( $name ) { $this->mHTMLtitle = $name; }
+
+ /**
+ * Set the new value of the "action text", this will be added to the
+ * "HTML title", separated from it with " - ".
+ *
+ * @param $text String: new value of the "action text"
+ */
+ public function setPageTitleActionText( $text ) {
+ $this->mPageTitleActionText = $text;
+ }
+
+ /**
+ * Get the value of the "action text"
+ *
+ * @return String
+ */
+ public function getPageTitleActionText() {
+ if ( isset( $this->mPageTitleActionText ) ) {
+ return $this->mPageTitleActionText;
+ }
+ }
+
+ /**
+ * "HTML title" means the contents of <title>.
+ * It is stored as plain, unescaped text and will be run through htmlspecialchars in the skin file.
+ * If $name is from page title, it can only override names which are also from page title,
+ * but if it is not from page title, it can override all other names.
+ */
+ public function setHTMLTitle( $name, $frompagetitle = false ) {
+ if ( $frompagetitle && $this->mHTMLtitleFromPagetitle ) {
+ $this->mHTMLtitle = $name;
+ }
+ elseif ( $this->mHTMLtitleFromPagetitle ) {
+ $this->mHTMLtitle = $name;
+ $this->mHTMLtitleFromPagetitle = false;
+ }
+ }
+
+ /**
+ * Return the "HTML title", i.e. the content of the <title> tag.
+ *
+ * @return String
+ */
+ public function getHTMLTitle() {
+ return $this->mHTMLtitle;
+ }
+
+ /**
+ * "Page title" means the contents of \<h1\>. It is stored as a valid HTML fragment.
+ * This function allows good tags like \<sup\> in the \<h1\> tag, but not bad tags like \<script\>.
+ * This function automatically sets \<title\> to the same content as \<h1\> but with all tags removed.
+ * Bad tags that were escaped in \<h1\> will still be escaped in \<title\>, and good tags like \<i\> will be dropped entirely.
+ */
public function setPageTitle( $name ) {
- global $wgContLang;
- $name = $wgContLang->convert( $name, true );
- $this->mPagetitle = $name;
+ # change "<script>foo&bar</script>" to "&lt;script&gt;foo&amp;bar&lt;/script&gt;"
+ # but leave "<i>foobar</i>" alone
+ $nameWithTags = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $name ) );
+ $this->mPagetitle = $nameWithTags;
$taction = $this->getPageTitleActionText();
if( !empty( $taction ) ) {
$name .= ' - '.$taction;
}
- $this->setHTMLTitle( wfMsg( 'pagetitle', $name ) );
+ # change "<i>foo&amp;bar</i>" to "foo&bar"
+ $this->setHTMLTitle( wfMsg( 'pagetitle', Sanitizer::stripAllTags( $nameWithTags ) ) );
}
- 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; }
- public function isPrintable() { return $this->mPrintable; }
- public function setSyndicated( $show = true ) { $this->mShowFeedLinks = $show; }
- public function isSyndicated() { return $this->mShowFeedLinks; }
- public function setFeedAppendQuery( $val ) { $this->mFeedLinksAppendQuery = $val; }
- public function getFeedAppendQuery() { return $this->mFeedLinksAppendQuery; }
- public function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; }
- public function getOnloadHandler() { return $this->mOnloadHandler; }
- public function disable() { $this->mDoNothing = true; }
- public function isDisabled() { return $this->mDoNothing; }
+ /**
+ * Return the "page title", i.e. the content of the \<h1\> tag.
+ *
+ * @return String
+ */
+ public function getPageTitle() {
+ return $this->mPagetitle;
+ }
- public function setArticleRelated( $v ) {
- $this->mIsArticleRelated = $v;
- if ( !$v ) {
- $this->mIsarticle = false;
+ /**
+ * Set the Title object to use
+ *
+ * @param $t Title object
+ */
+ public function setTitle( $t ) {
+ $this->mTitle = $t;
+ }
+
+ /**
+ * Get the Title object used in this instance
+ *
+ * @return Title
+ */
+ public function getTitle() {
+ if ( $this->mTitle instanceof Title ) {
+ return $this->mTitle;
+ } else {
+ wfDebug( __METHOD__ . ' called and $mTitle is null. Return $wgTitle for sanity' );
+ global $wgTitle;
+ return $wgTitle;
+ }
+ }
+
+ /**
+ * Replace the subtile with $str
+ *
+ * @param $str String: new value of the subtitle
+ */
+ public function setSubtitle( $str ) {
+ $this->mSubtitle = /*$this->parse(*/ $str /*)*/; // @bug 2514
+ }
+
+ /**
+ * Add $str to the subtitle
+ *
+ * @param $str String to add to the subtitle
+ */
+ public function appendSubtitle( $str ) {
+ $this->mSubtitle .= /*$this->parse(*/ $str /*)*/; // @bug 2514
+ }
+
+ /**
+ * Get the subtitle
+ *
+ * @return String
+ */
+ public function getSubtitle() {
+ return $this->mSubtitle;
+ }
+
+
+ /**
+ * Set the page as printable, i.e. it'll be displayed with with all
+ * print styles included
+ */
+ public function setPrintable() {
+ $this->mPrintable = true;
+ }
+
+ /**
+ * Return whether the page is "printable"
+ *
+ * @return Boolean
+ */
+ public function isPrintable() {
+ return $this->mPrintable;
+ }
+
+
+ /**
+ * Disable output completely, i.e. calling output() will have no effect
+ */
+ public function disable() {
+ $this->mDoNothing = true;
+ }
+
+ /**
+ * Return whether the output will be completely disabled
+ *
+ * @return Boolean
+ */
+ public function isDisabled() {
+ return $this->mDoNothing;
+ }
+
+
+ /**
+ * Show an "add new section" link?
+ *
+ * @return Boolean
+ */
+ public function showNewSectionLink() {
+ return $this->mNewSectionLink;
+ }
+
+ /**
+ * Forcibly hide the new section link?
+ *
+ * @return Boolean
+ */
+ public function forceHideNewSectionLink() {
+ return $this->mHideNewSectionLink;
+ }
+
+
+ /**
+ * Add or remove feed links in the page header
+ * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
+ * for the new version
+ * @see addFeedLink()
+ *
+ * @param $show Boolean: true: add default feeds, false: remove all feeds
+ */
+ public function setSyndicated( $show = true ) {
+ if ( $show ) {
+ $this->setFeedAppendQuery( false );
+ } else {
+ $this->mFeedLinks = array();
}
}
+
+ /**
+ * Add default feeds to the page header
+ * This is mainly kept for backward compatibility, see OutputPage::addFeedLink()
+ * for the new version
+ * @see addFeedLink()
+ *
+ * @param $val String: query to append to feed links or false to output
+ * default links
+ */
+ public function setFeedAppendQuery( $val ) {
+ global $wgAdvertisedFeedTypes;
+
+ $this->mFeedLinks = array();
+
+ foreach ( $wgAdvertisedFeedTypes as $type ) {
+ $query = "feed=$type";
+ if ( is_string( $val ) ) {
+ $query .= '&' . $val;
+ }
+ $this->mFeedLinks[$type] = $this->getTitle()->getLocalURL( $query );
+ }
+ }
+
+ /**
+ * Add a feed link to the page header
+ *
+ * @param $format String: feed type, should be a key of $wgFeedClasses
+ * @param $href String: URL
+ */
+ public function addFeedLink( $format, $href ) {
+ $this->mFeedLinks[$format] = $href;
+ }
+
+ /**
+ * Should we output feed links for this page?
+ * @return Boolean
+ */
+ public function isSyndicated() {
+ return count( $this->mFeedLinks ) > 0;
+ }
+
+ /**
+ * Return URLs for each supported syndication format for this page.
+ * @return array associating format keys with URLs
+ */
+ public function getSyndicationLinks() {
+ return $this->mFeedLinks;
+ }
+
+ /**
+ * Will currently always return null
+ *
+ * @return null
+ */
+ public function getFeedAppendQuery() {
+ return $this->mFeedLinksAppendQuery;
+ }
+
+ /**
+ * Set whether the displayed content is related to the source of the
+ * corresponding article on the wiki
+ * Setting true will cause the change "article related" toggle to true
+ *
+ * @param $v Boolean
+ */
public function setArticleFlag( $v ) {
$this->mIsarticle = $v;
if ( $v ) {
@@ -355,22 +683,73 @@ class OutputPage {
}
}
- public function isArticleRelated() { return $this->mIsArticleRelated; }
+ /**
+ * Return whether the content displayed page is related to the source of
+ * the corresponding article on the wiki
+ *
+ * @return Boolean
+ */
+ public function isArticle() {
+ return $this->mIsarticle;
+ }
- public function getLanguageLinks() { return $this->mLanguageLinks; }
- public function addLanguageLinks($newLinkArray) {
+ /**
+ * Set whether this page is related an article on the wiki
+ * Setting false will cause the change of "article flag" toggle to false
+ *
+ * @param $v Boolean
+ */
+ public function setArticleRelated( $v ) {
+ $this->mIsArticleRelated = $v;
+ if ( !$v ) {
+ $this->mIsarticle = false;
+ }
+ }
+
+ /**
+ * Return whether this page is related an article on the wiki
+ *
+ * @return Boolean
+ */
+ public function isArticleRelated() {
+ return $this->mIsArticleRelated;
+ }
+
+
+ /**
+ * Add new language links
+ *
+ * @param $newLinkArray Associative array mapping language code to the page
+ * name
+ */
+ public function addLanguageLinks( $newLinkArray ) {
$this->mLanguageLinks += $newLinkArray;
}
- public function setLanguageLinks($newLinkArray) {
+
+ /**
+ * Reset the language links and add new language links
+ *
+ * @param $newLinkArray Associative array mapping language code to the page
+ * name
+ */
+ public function setLanguageLinks( $newLinkArray ) {
$this->mLanguageLinks = $newLinkArray;
}
- public function getCategoryLinks() {
- return $this->mCategoryLinks;
+ /**
+ * Get the list of language links
+ *
+ * @return Associative array mapping language code to the page name
+ */
+ public function getLanguageLinks() {
+ return $this->mLanguageLinks;
}
+
/**
* Add an array of categories, with names in the keys
+ *
+ * @param $categories Associative array mapping category name to its sort key
*/
public function addCategoryLinks( $categories ) {
global $wgUser, $wgContLang;
@@ -418,34 +797,139 @@ class OutputPage {
if ( array_key_exists( $category, $categories ) )
continue;
$text = $wgContLang->convertHtml( $title->getText() );
- $this->mCategoryLinks[$type][] = $sk->makeLinkObj( $title, $text );
+ $this->mCategories[] = $title->getText();
+ $this->mCategoryLinks[$type][] = $sk->link( $title, $text );
}
}
}
- public function setCategoryLinks($categories) {
+ /**
+ * Reset the category links (but not the category list) and add $categories
+ *
+ * @param $categories Associative array mapping category name to its sort key
+ */
+ public function setCategoryLinks( $categories ) {
$this->mCategoryLinks = array();
- $this->addCategoryLinks($categories);
+ $this->addCategoryLinks( $categories );
+ }
+
+ /**
+ * Get the list of category links, in a 2-D array with the following format:
+ * $arr[$type][] = $link, where $type is either "normal" or "hidden" (for
+ * hidden categories) and $link a HTML fragment with a link to the category
+ * page
+ *
+ * @return Array
+ */
+ public function getCategoryLinks() {
+ return $this->mCategoryLinks;
+ }
+
+ /**
+ * Get the list of category names this page belongs to
+ *
+ * @return Array of strings
+ */
+ public function getCategories() {
+ return $this->mCategories;
+ }
+
+
+ /**
+ * Suppress the quickbar from the output, only for skin supporting
+ * the quickbar
+ */
+ public function suppressQuickbar() {
+ $this->mSuppressQuickbar = true;
+ }
+
+ /**
+ * Return whether the quickbar should be suppressed from the output
+ *
+ * @return Boolean
+ */
+ public function isQuickbarSuppressed() {
+ return $this->mSuppressQuickbar;
+ }
+
+
+ /**
+ * Remove user JavaScript from scripts to load
+ */
+ public function disallowUserJs() {
+ $this->mAllowUserJs = false;
+ }
+
+ /**
+ * Return whether user JavaScript is allowed for this page
+ *
+ * @return Boolean
+ */
+ public function isUserJsAllowed() {
+ return $this->mAllowUserJs;
+ }
+
+
+ /**
+ * Prepend $text to the body HTML
+ *
+ * @param $text String: HTML
+ */
+ public function prependHTML( $text ) {
+ $this->mBodytext = $text . $this->mBodytext;
+ }
+
+ /**
+ * Append $text to the body HTML
+ *
+ * @param $text String: HTML
+ */
+ public function addHTML( $text ) {
+ $this->mBodytext .= $text;
+ }
+
+ /**
+ * Clear the body HTML
+ */
+ public function clearHTML() {
+ $this->mBodytext = '';
}
- public function suppressQuickbar() { $this->mSuppressQuickbar = true; }
- public function isQuickbarSuppressed() { return $this->mSuppressQuickbar; }
+ /**
+ * Get the body HTML
+ *
+ * @return String: HTML
+ */
+ public function getHTML() {
+ return $this->mBodytext;
+ }
- public function disallowUserJs() { $this->mAllowUserJs = false; }
- public function isUserJsAllowed() { return $this->mAllowUserJs; }
- public function prependHTML( $text ) { $this->mBodytext = $text . $this->mBodytext; }
- public function addHTML( $text ) { $this->mBodytext .= $text; }
- public function clearHTML() { $this->mBodytext = ''; }
- public function getHTML() { return $this->mBodytext; }
- public function debug( $text ) { $this->mDebugtext .= $text; }
+ /**
+ * Add $text to the debug output
+ *
+ * @param $text String: debug text
+ */
+ public function debug( $text ) {
+ $this->mDebugtext .= $text;
+ }
- /* @deprecated */
+
+ /**
+ * @deprecated use parserOptions() instead
+ */
public function setParserOptions( $options ) {
wfDeprecated( __METHOD__ );
return $this->parserOptions( $options );
}
+ /**
+ * Get/set the ParserOptions object to use for wikitext parsing
+ *
+ * @param $options either the ParserOption to use or null to only get the
+ * current ParserOption object
+ * @return current ParserOption object
+ */
public function parserOptions( $options = null ) {
if ( !$this->mParserOptions ) {
$this->mParserOptions = new ParserOptions;
@@ -456,40 +940,78 @@ class OutputPage {
/**
* Set the revision ID which will be seen by the wiki text parser
* for things such as embedded {{REVISIONID}} variable use.
- * @param mixed $revid an integer, or NULL
- * @return mixed previous value
+ *
+ * @param $revid Mixed: an positive integer, or null
+ * @return Mixed: previous value
*/
public function setRevisionId( $revid ) {
$val = is_null( $revid ) ? null : intval( $revid );
return wfSetVar( $this->mRevisionId, $val );
}
-
+
+ /**
+ * Get the current revision ID
+ *
+ * @return Integer
+ */
public function getRevisionId() {
return $this->mRevisionId;
}
/**
* Convert wikitext to HTML and add it to the buffer
- * Default assumes that the current page title will
- * be used.
+ * Default assumes that the current page title will be used.
*
- * @param string $text
- * @param bool $linestart
+ * @param $text String
+ * @param $linestart Boolean: is this the start of a line?
*/
public function addWikiText( $text, $linestart = true ) {
- global $wgTitle;
- $this->addWikiTextTitle($text, $wgTitle, $linestart);
+ $title = $this->getTitle(); // Work arround E_STRICT
+ $this->addWikiTextTitle( $text, $title, $linestart );
}
- public function addWikiTextWithTitle($text, &$title, $linestart = true) {
- $this->addWikiTextTitle($text, $title, $linestart);
+ /**
+ * Add wikitext with a custom Title object
+ *
+ * @param $text String: wikitext
+ * @param $title Title object
+ * @param $linestart Boolean: is this the start of a line?
+ */
+ public function addWikiTextWithTitle( $text, &$title, $linestart = true ) {
+ $this->addWikiTextTitle( $text, $title, $linestart );
}
- function addWikiTextTitleTidy($text, &$title, $linestart = true) {
+ /**
+ * Add wikitext with a custom Title object and
+ *
+ * @param $text String: wikitext
+ * @param $title Title object
+ * @param $linestart Boolean: is this the start of a line?
+ */
+ function addWikiTextTitleTidy( $text, &$title, $linestart = true ) {
$this->addWikiTextTitle( $text, $title, $linestart, true );
}
- public function addWikiTextTitle($text, &$title, $linestart, $tidy = false) {
+ /**
+ * Add wikitext with tidy enabled
+ *
+ * @param $text String: wikitext
+ * @param $linestart Boolean: is this the start of a line?
+ */
+ public function addWikiTextTidy( $text, $linestart = true ) {
+ $title = $this->getTitle();
+ $this->addWikiTextTitleTidy($text, $title, $linestart);
+ }
+
+ /**
+ * Add wikitext with a custom Title object
+ *
+ * @param $text String: wikitext
+ * @param $title Title object
+ * @param $linestart Boolean: is this the start of a line?
+ * @param $tidy Boolean: whether to use tidy
+ */
+ public function addWikiTextTitle( $text, &$title, $linestart, $tidy = false ) {
global $wgParser;
wfProfileIn( __METHOD__ );
@@ -510,35 +1032,60 @@ class OutputPage {
}
/**
- * @todo document
- * @param ParserOutput object &$parserOutput
+ * Add wikitext to the buffer, assuming that this is the primary text for a page view
+ * Saves the text into the parser cache if possible.
+ *
+ * @param $text String: wikitext
+ * @param $article Article object
+ * @param $cache Boolean
+ * @deprecated Use Article::outputWikitext
+ */
+ public function addPrimaryWikiText( $text, $article, $cache = true ) {
+ global $wgParser;
+
+ 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->save( $parserOutput, $article, $popts);
+ }
+
+ $this->addParserOutput( $parserOutput );
+ }
+
+ /**
+ * @deprecated use addWikiTextTidy()
+ */
+ public function addSecondaryWikiText( $text, $linestart = true ) {
+ wfDeprecated( __METHOD__ );
+ $this->addWikiTextTitleTidy($text, $this->getTitle(), $linestart);
+ }
+
+
+ /**
+ * Add a ParserOutput object, but without Html
+ *
+ * @param $parserOutput ParserOutput object
*/
public function addParserOutputNoText( &$parserOutput ) {
- global $wgTitle, $wgExemptFromUserRobotsControl, $wgContentNamespaces;
+ global $wgExemptFromUserRobotsControl, $wgContentNamespaces;
$this->mLanguageLinks += $parserOutput->getLanguageLinks();
$this->addCategoryLinks( $parserOutput->getCategories() );
$this->mNewSectionLink = $parserOutput->getNewSection();
$this->mHideNewSectionLink = $parserOutput->getHideNewSection();
- if( is_null( $wgExemptFromUserRobotsControl ) ) {
- $bannedNamespaces = $wgContentNamespaces;
- } else {
- $bannedNamespaces = $wgExemptFromUserRobotsControl;
- }
- if( !in_array( $wgTitle->getNamespace(), $bannedNamespaces ) ) {
- # FIXME (bug 14900): This overrides $wgArticleRobotPolicies, and it
- # shouldn't
- $this->setIndexPolicy( $parserOutput->getIndexPolicy() );
- }
-
- $this->addKeywords( $parserOutput );
$this->mParseWarnings = $parserOutput->getWarnings();
if ( $parserOutput->getCacheTime() == -1 ) {
$this->enableClientCache( false );
}
$this->mNoGallery = $parserOutput->getNoGallery();
- $this->mHeadItems = array_merge( $this->mHeadItems, (array)$parserOutput->mHeadItems );
+ $this->mHeadItems = array_merge( $this->mHeadItems, $parserOutput->getHeadItems() );
// Versioning...
foreach ( (array)$parserOutput->mTemplateIds as $ns => $dbks ) {
if ( isset( $this->mTemplateIds[$ns] ) ) {
@@ -548,10 +1095,10 @@ class OutputPage {
}
}
// Page title
- if( ( $dt = $parserOutput->getDisplayTitle() ) !== false )
- $this->setPageTitle( $dt );
- else if ( ( $title = $parserOutput->getTitleText() ) != '' )
+ $title = $parserOutput->getTitleText();
+ if ( $title != '' ) {
$this->setPageTitle( $title );
+ }
// Hooks registered in the object
global $wgParserOutputHooks;
@@ -566,65 +1113,22 @@ class OutputPage {
}
/**
- * @todo document
- * @param ParserOutput &$parserOutput
+ * Add a ParserOutput object
+ *
+ * @param $parserOutput ParserOutput
*/
function addParserOutput( &$parserOutput ) {
$this->addParserOutputNoText( $parserOutput );
- $text = $parserOutput->getText();
+ $text = $parserOutput->getText();
wfRunHooks( 'OutputPageBeforeHTML',array( &$this, &$text ) );
$this->addHTML( $text );
}
- /**
- * Add wikitext to the buffer, assuming that this is the primary text for a page view
- * Saves the text into the parser cache if possible.
- *
- * @param string $text
- * @param Article $article
- * @param bool $cache
- * @deprecated Use Article::outputWikitext
- */
- 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->save( $parserOutput, $article, $popts);
- }
-
- $this->addParserOutput( $parserOutput );
- }
-
- /**
- * @deprecated use addWikiTextTidy()
- */
- public function addSecondaryWikiText( $text, $linestart = true ) {
- global $wgTitle;
- wfDeprecated( __METHOD__ );
- $this->addWikiTextTitleTidy($text, $wgTitle, $linestart);
- }
-
- /**
- * Add wikitext with tidy enabled
- */
- public function addWikiTextTidy( $text, $linestart = true ) {
- global $wgTitle;
- $this->addWikiTextTitleTidy($text, $wgTitle, $linestart);
- }
-
/**
* Add the output of a QuickTemplate to the output buffer
*
- * @param QuickTemplate $template
+ * @param $template QuickTemplate
*/
public function addTemplate( &$template ) {
ob_start();
@@ -636,24 +1140,36 @@ class OutputPage {
/**
* Parse wikitext and return the HTML.
*
- * @param string $text
- * @param bool $linestart Is this the start of a line?
- * @param bool $interface ??
+ * @param $text String
+ * @param $linestart Boolean: is this the start of a line?
+ * @param $interface Boolean: use interface language ($wgLang instead of
+ * $wgContLang) while parsing language sensitive magic
+ * words like GRAMMAR and PLURAL
+ * @return String: HTML
*/
public function parse( $text, $linestart = true, $interface = false ) {
- global $wgParser, $wgTitle;
- if( is_null( $wgTitle ) ) {
- throw new MWException( 'Empty $wgTitle in ' . __METHOD__ );
+ global $wgParser;
+ if( is_null( $this->getTitle() ) ) {
+ throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
}
$popts = $this->parserOptions();
if ( $interface) { $popts->setInterfaceMessage(true); }
- $parserOutput = $wgParser->parse( $text, $wgTitle, $popts,
+ $parserOutput = $wgParser->parse( $text, $this->getTitle(), $popts,
$linestart, true, $this->mRevisionId );
if ( $interface) { $popts->setInterfaceMessage(false); }
return $parserOutput->getText();
}
- /** Parse wikitext, strip paragraphs, and return the HTML. */
+ /**
+ * Parse wikitext, strip paragraphs, and return the HTML.
+ *
+ * @param $text String
+ * @param $linestart Boolean: is this the start of a line?
+ * @param $interface Boolean: use interface language ($wgLang instead of
+ * $wgContLang) while parsing language sensitive magic
+ * words like GRAMMAR and PLURAL
+ * @return String: HTML
+ */
public function parseInline( $text, $linestart = true, $interface = false ) {
$parsed = $this->parse( $text, $linestart, $interface );
@@ -666,15 +1182,16 @@ class OutputPage {
}
/**
- * @param Article $article
- * @param User $user
+ * @deprecated
*
- * @return bool True if successful, else false.
+ * @param $article Article
+ * @return Boolean: true if successful, else false.
*/
public function tryParserCache( &$article ) {
- $parserCache = ParserCache::singleton();
- $parserOutput = $parserCache->get( $article, $this->parserOptions() );
- if ( $parserOutput !== false ) {
+ wfDeprecated( __METHOD__ );
+ $parserOutput = ParserCache::singleton()->get( $article, $article->getParserOptions() );
+
+ if ($parserOutput !== false) {
$this->addParserOutput( $parserOutput );
return true;
} else {
@@ -683,7 +1200,9 @@ class OutputPage {
}
/**
- * @param int $maxage Maximum cache time on the Squid, in seconds.
+ * Set the value of the "s-maxage" part of the "Cache-control" HTTP header
+ *
+ * @param $maxage Integer: maximum cache time on the Squid, in seconds.
*/
public function setSquidMaxage( $maxage ) {
$this->mSquidMaxage = $maxage;
@@ -691,12 +1210,18 @@ class OutputPage {
/**
* Use enableClientCache(false) to force it to send nocache headers
+ *
* @param $state ??
*/
public function enableClientCache( $state ) {
return wfSetVar( $this->mEnableClientCache, $state );
}
+ /**
+ * Get the list of cookies that will influence on the cache
+ *
+ * @return Array
+ */
function getCacheVaryCookies() {
global $wgCookiePrefix, $wgCacheVaryCookies;
static $cookies;
@@ -714,15 +1239,23 @@ class OutputPage {
return $cookies;
}
+ /**
+ * Return whether this page is not cacheable because "useskin" or "uselang"
+ * url parameters were passed
+ *
+ * @return Boolean
+ */
function uncacheableBecauseRequestVars() {
global $wgRequest;
- return $wgRequest->getText('useskin', false) === false
+ 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
+ *
+ * @return Boolean
*/
function haveCacheVaryCookies() {
global $wgRequest;
@@ -742,35 +1275,98 @@ class OutputPage {
return false;
}
- /** Get a complete X-Vary-Options header */
+ /**
+ * Add an HTTP header that will influence on the cache
+ *
+ * @param $header String: header name
+ * @param $option either an Array or null
+ */
+ public function addVaryHeader( $header, $option = null ) {
+ if ( !array_key_exists( $header, $this->mVaryHeader ) ) {
+ $this->mVaryHeader[$header] = $option;
+ }
+ elseif( is_array( $option ) ) {
+ if( is_array( $this->mVaryHeader[$header] ) ) {
+ $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option );
+ }
+ else {
+ $this->mVaryHeader[$header] = $option;
+ }
+ }
+ $this->mVaryHeader[$header] = array_unique( $this->mVaryHeader[$header] );
+ }
+
+ /**
+ * Get a complete X-Vary-Options header
+ *
+ * @return String
+ */
public function getXVO() {
$cvCookies = $this->getCacheVaryCookies();
- $xvo = 'X-Vary-Options: Accept-Encoding;list-contains=gzip,Cookie;';
- $first = true;
+
+ $cookiesOption = array();
foreach ( $cvCookies as $cookieName ) {
- if ( $first ) {
- $first = false;
- } else {
- $xvo .= ';';
- }
- $xvo .= 'string-contains=' . $cookieName;
+ $cookiesOption[] = 'string-contains=' . $cookieName;
}
+ $this->addVaryHeader( 'Cookie', $cookiesOption );
+
+ $headers = array();
+ foreach( $this->mVaryHeader as $header => $option ) {
+ $newheader = $header;
+ if( is_array( $option ) )
+ $newheader .= ';' . implode( ';', $option );
+ $headers[] = $newheader;
+ }
+ $xvo = 'X-Vary-Options: ' . implode( ',', $headers );
+
return $xvo;
}
+ /**
+ * bug 21672: Add Accept-Language to Vary and XVO headers
+ * if there's no 'variant' parameter existed in GET.
+ *
+ * For example:
+ * /w/index.php?title=Main_page should always be served; but
+ * /w/index.php?title=Main_page&variant=zh-cn should never be served.
+ *
+ * patched by Liangent and Philip
+ */
+ function addAcceptLanguage() {
+ global $wgRequest, $wgContLang;
+ if( !$wgRequest->getCheck('variant') && $wgContLang->hasVariants() ) {
+ $variants = $wgContLang->getVariants();
+ $aloption = array();
+ foreach ( $variants as $variant ) {
+ if( $variant === $wgContLang->getCode() )
+ continue;
+ else
+ $aloption[] = "string-contains=$variant";
+ }
+ $this->addVaryHeader( 'Accept-Language', $aloption );
+ }
+ }
+
+ /**
+ * Send cache control HTTP headers
+ */
public function sendCacheControl() {
- global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest;
+ global $wgUseSquid, $wgUseESI, $wgUseETag, $wgSquidMaxage, $wgRequest, $wgUseXVO;
$response = $wgRequest->response();
if ($wgUseETag && $this->mETag)
$response->header("ETag: $this->mETag");
+ $this->addAcceptLanguage();
+
# don't serve compressed data to clients who can't handle it
# maintain different caches for logged-in users and non-logged in ones
- $response->header( 'Vary: Accept-Encoding, Cookie' );
+ $response->header( 'Vary: ' . join( ', ', array_keys( $this->mVaryHeader ) ) );
- # Add an X-Vary-Options header for Squid with Wikimedia patches
- $response->header( $this->getXVO() );
+ if ( $wgUseXVO ) {
+ # Add an X-Vary-Options header for Squid with Wikimedia patches
+ $response->header( $this->getXVO() );
+ }
if( !$this->uncacheableBecauseRequestVars() && $this->mEnableClientCache ) {
if( $wgUseSquid && session_id() == '' &&
@@ -817,99 +1413,106 @@ class OutputPage {
}
/**
+ * Get the message associed with the HTTP response code $code
+ *
+ * @param $code Integer: status code
+ * @return String or null: message or null if $code is not in the list of
+ * messages
+ */
+ public static function getStatusMessage( $code ) {
+ static $statusMessage = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Request Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 507 => 'Insufficient Storage'
+ );
+ return isset( $statusMessage[$code] ) ? $statusMessage[$code] : null;
+ }
+
+ /**
* Finally, all the text has been munged and accumulated into
* the object, let's actually output it:
*/
public function output() {
global $wgUser, $wgOutputEncoding, $wgRequest;
global $wgContLanguageCode, $wgDebugRedirects, $wgMimeType;
- global $wgJsMimeType, $wgUseAjax, $wgAjaxWatch;
+ global $wgUseAjax, $wgAjaxWatch;
global $wgEnableMWSuggest, $wgUniversalEditButton;
- global $wgArticle, $wgTitle;
+ global $wgArticle;
if( $this->mDoNothing ){
return;
}
-
wfProfileIn( __METHOD__ );
-
- if ( '' != $this->mRedirect ) {
+ if ( $this->mRedirect != '' ) {
# Standards require redirect URLs to be absolute
$this->mRedirect = wfExpandUrl( $this->mRedirect );
- if( $this->mRedirectCode == '301') {
+ if( $this->mRedirectCode == '301' || $this->mRedirectCode == '303' ) {
if( !$wgDebugRedirects ) {
- $wgRequest->response()->header("HTTP/1.1 {$this->mRedirectCode} Moved Permanently");
+ $message = self::getStatusMessage( $this->mRedirectCode );
+ $wgRequest->response()->header( "HTTP/1.1 {$this->mRedirectCode} $message" );
}
$this->mLastModified = wfTimestamp( TS_RFC2822 );
}
-
$this->sendCacheControl();
- $wgRequest->response()->header("Content-Type: text/html; charset=utf-8");
+ $wgRequest->response()->header( "Content-Type: text/html; charset=utf-8" );
if( $wgDebugRedirects ) {
$url = htmlspecialchars( $this->mRedirect );
print "<html>\n<head>\n<title>Redirect</title>\n</head>\n<body>\n";
print "<p>Location: <a href=\"$url\">$url</a></p>\n";
print "</body>\n</html>\n";
} else {
- $wgRequest->response()->header( 'Location: '.$this->mRedirect );
+ $wgRequest->response()->header( 'Location: ' . $this->mRedirect );
}
wfProfileOut( __METHOD__ );
return;
- }
- elseif ( $this->mStatusCode )
- {
- $statusMessage = array(
- 100 => 'Continue',
- 101 => 'Switching Protocols',
- 102 => 'Processing',
- 200 => 'OK',
- 201 => 'Created',
- 202 => 'Accepted',
- 203 => 'Non-Authoritative Information',
- 204 => 'No Content',
- 205 => 'Reset Content',
- 206 => 'Partial Content',
- 207 => 'Multi-Status',
- 300 => 'Multiple Choices',
- 301 => 'Moved Permanently',
- 302 => 'Found',
- 303 => 'See Other',
- 304 => 'Not Modified',
- 305 => 'Use Proxy',
- 307 => 'Temporary Redirect',
- 400 => 'Bad Request',
- 401 => 'Unauthorized',
- 402 => 'Payment Required',
- 403 => 'Forbidden',
- 404 => 'Not Found',
- 405 => 'Method Not Allowed',
- 406 => 'Not Acceptable',
- 407 => 'Proxy Authentication Required',
- 408 => 'Request Timeout',
- 409 => 'Conflict',
- 410 => 'Gone',
- 411 => 'Length Required',
- 412 => 'Precondition Failed',
- 413 => 'Request Entity Too Large',
- 414 => 'Request-URI Too Large',
- 415 => 'Unsupported Media Type',
- 416 => 'Request Range Not Satisfiable',
- 417 => 'Expectation Failed',
- 422 => 'Unprocessable Entity',
- 423 => 'Locked',
- 424 => 'Failed Dependency',
- 500 => 'Internal Server Error',
- 501 => 'Not Implemented',
- 502 => 'Bad Gateway',
- 503 => 'Service Unavailable',
- 504 => 'Gateway Timeout',
- 505 => 'HTTP Version Not Supported',
- 507 => 'Insufficient Storage'
- );
-
- if ( $statusMessage[$this->mStatusCode] )
- $wgRequest->response()->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $statusMessage[$this->mStatusCode] );
+ } elseif ( $this->mStatusCode ) {
+ $message = self::getStatusMessage( $this->mStatusCode );
+ if ( $message )
+ $wgRequest->response()->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $message );
}
$sk = $wgUser->getSkin();
@@ -922,35 +1525,36 @@ class OutputPage {
if( $wgAjaxWatch && $wgUser->isLoggedIn() ) {
$this->addScriptFile( 'ajaxwatch.js' );
}
-
+
if ( $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false ) ){
$this->addScriptFile( 'mwsuggest.js' );
}
}
-
+
if( $wgUser->getBoolOption( 'editsectiononrightclick' ) ) {
$this->addScriptFile( 'rightclickedit.js' );
}
if( $wgUniversalEditButton ) {
- if( isset( $wgArticle ) && isset( $wgTitle ) && $wgTitle->quickUserCan( 'edit' )
- && ( $wgTitle->exists() || $wgTitle->quickUserCan( 'create' ) ) ) {
+ if( isset( $wgArticle ) && $this->getTitle() && $this->getTitle()->quickUserCan( 'edit' )
+ && ( $this->getTitle()->exists() || $this->getTitle()->quickUserCan( 'create' ) ) ) {
// Original UniversalEditButton
+ $msg = wfMsg('edit');
$this->addLink( array(
'rel' => 'alternate',
'type' => 'application/x-wiki',
- 'title' => wfMsg( 'edit' ),
- 'href' => $wgTitle->getLocalURL( 'action=edit' )
+ 'title' => $msg,
+ 'href' => $this->getTitle()->getLocalURL( 'action=edit' )
) );
// Alternate edit link
$this->addLink( array(
'rel' => 'edit',
- 'title' => wfMsg( 'edit' ),
- 'href' => $wgTitle->getLocalURL( 'action=edit' )
+ 'title' => $msg,
+ 'href' => $this->getTitle()->getLocalURL( 'action=edit' )
) );
}
}
-
+
# Buffer output; final headers may depend on later processing
ob_start();
@@ -975,8 +1579,10 @@ class OutputPage {
}
/**
- * @todo document
- * @param string $ins
+ * Actually output something with print(). Performs an iconv to the
+ * output encoding, if needed.
+ *
+ * @param $ins String: the string to output
*/
public function out( $ins ) {
global $wgInputEncoding, $wgOutputEncoding, $wgContLang;
@@ -994,7 +1600,7 @@ class OutputPage {
*/
public static function setEncodings() {
global $wgInputEncoding, $wgOutputEncoding;
- global $wgUser, $wgContLang;
+ global $wgContLang;
$wgInputEncoding = strtolower( $wgInputEncoding );
@@ -1006,9 +1612,9 @@ class OutputPage {
}
/**
- * Deprecated, use wfReportTime() instead.
- * @return string
- * @deprecated
+ * @deprecated use wfReportTime() instead.
+ *
+ * @return String
*/
public function reportTime() {
wfDeprecated( __METHOD__ );
@@ -1019,11 +1625,11 @@ class OutputPage {
/**
* Produce a "user is blocked" page.
*
- * @param bool $return Whether to have a "return to $wgTitle" message or not.
+ * @param $return Boolean: whether to have a "return to $wgTitle" message or not.
* @return nothing
*/
function blockedPage( $return = true ) {
- global $wgUser, $wgContLang, $wgTitle, $wgLang;
+ global $wgUser, $wgContLang, $wgLang;
$this->setPageTitle( wfMsg( 'blockedtitle' ) );
$this->setRobotPolicy( 'noindex,nofollow' );
@@ -1073,7 +1679,7 @@ class OutputPage {
# Don't auto-return to special pages
if( $return ) {
- $return = $wgTitle->getNamespace() > -1 ? $wgTitle : NULL;
+ $return = $this->getTitle()->getNamespace() > -1 ? $this->getTitle() : null;
$this->returnToMain( null, $return );
}
}
@@ -1081,14 +1687,13 @@ class OutputPage {
/**
* Output a standard error page
*
- * @param string $title Message key for page title
- * @param string $msg Message key for page text
- * @param array $params Message parameters
+ * @param $title String: message key for page title
+ * @param $msg String: message key for page text
+ * @param $params Array: message parameters
*/
public function showErrorPage( $title, $msg, $params = array() ) {
- global $wgTitle;
- if ( isset($wgTitle) ) {
- $this->mDebugtext .= 'Original title: ' . $wgTitle->getPrefixedText() . "\n";
+ if ( $this->getTitle() ) {
+ $this->mDebugtext .= 'Original title: ' . $this->getTitle()->getPrefixedText() . "\n";
}
$this->setPageTitle( wfMsg( $title ) );
$this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
@@ -1108,14 +1713,12 @@ class OutputPage {
/**
* Output a standard permission error page
*
- * @param array $errors Error message keys
+ * @param $errors Array: error message keys
+ * @param $action String: action that was denied or null if unknown
*/
- public function showPermissionsErrorPage( $errors, $action = null )
- {
- global $wgTitle;
-
+ public function showPermissionsErrorPage( $errors, $action = null ) {
$this->mDebugtext .= 'Original title: ' .
- $wgTitle->getPrefixedText() . "\n";
+ $this->getTitle()->getPrefixedText() . "\n";
$this->setPageTitle( wfMsg( 'permissionserrors' ) );
$this->setHTMLTitle( wfMsg( 'permissionserrors' ) );
$this->setRobotPolicy( 'noindex,nofollow' );
@@ -1126,17 +1729,11 @@ class OutputPage {
$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
*
- * @param mixed $version The version of MediaWiki needed to use the page
+ * @param $version Mixed: the version of MediaWiki needed to use the page
*/
public function versionRequired( $version ) {
$this->setPageTitle( wfMsg( 'versionrequired', $version ) );
@@ -1152,10 +1749,10 @@ class OutputPage {
/**
* Display an error page noting that a given permission bit is required.
*
- * @param string $permission key required
+ * @param $permission String: key required
*/
public function permissionRequired( $permission ) {
- global $wgUser, $wgLang;
+ global $wgLang;
$this->setPageTitle( wfMsg( 'badaccess' ) );
$this->setHTMLTitle( wfMsg( 'errorpagetitle' ) );
@@ -1176,16 +1773,14 @@ class OutputPage {
}
/**
- * Use permissionRequired.
- * @deprecated
+ * @deprecated use permissionRequired()
*/
public function sysopRequired() {
throw new MWException( "Call to deprecated OutputPage::sysopRequired() method\n" );
}
/**
- * Use permissionRequired.
- * @deprecated
+ * @deprecated use permissionRequired()
*/
public function developerRequired() {
throw new MWException( "Call to deprecated OutputPage::developerRequired() method\n" );
@@ -1195,7 +1790,7 @@ class OutputPage {
* Produce the stock "please login to use the wiki" page
*/
public function loginToUse() {
- global $wgUser, $wgTitle, $wgContLang;
+ global $wgUser, $wgContLang;
if( $wgUser->isLoggedIn() ) {
$this->permissionRequired( 'read' );
@@ -1210,9 +1805,15 @@ class OutputPage {
$this->setArticleFlag( false );
$loginTitle = SpecialPage::getTitleFor( 'Userlogin' );
- $loginLink = $skin->makeKnownLinkObj( $loginTitle, wfMsgHtml( 'loginreqlink' ), 'returnto=' . $wgTitle->getPrefixedUrl() );
+ $loginLink = $skin->link(
+ $loginTitle,
+ wfMsgHtml( 'loginreqlink' ),
+ array(),
+ array( 'returnto' => $this->getTitle()->getPrefixedText() ),
+ array( 'known', 'noclasses' )
+ );
$this->addHTML( wfMsgWikiHtml( 'loginreqpagetext', $loginLink ) );
- $this->addHTML( "\n<!--" . $wgTitle->getPrefixedUrl() . "-->" );
+ $this->addHTML( "\n<!--" . $this->getTitle()->getPrefixedUrl() . "-->" );
# Don't return to the main page if the user can't read it
# otherwise we'll end up in a pointless loop
@@ -1221,21 +1822,19 @@ class OutputPage {
$this->returnToMain( null, $mainPage );
}
- /** @deprecated */
- public function databaseError( $fname, $sql, $error, $errno ) {
- throw new MWException( "OutputPage::databaseError is obsolete\n" );
- }
-
/**
- * @param array $errors An array of arrays returned by Title::getUserPermissionsErrors
- * @return string The wikitext error-messages, formatted into a list.
+ * Format a list of error messages
+ *
+ * @param $errors An array of arrays returned by Title::getUserPermissionsErrors
+ * @param $action String: action that was denied or null if unknown
+ * @return String: the wikitext error-messages, formatted into a list.
*/
public function formatPermissionsErrorMessage( $errors, $action = null ) {
if ($action == null) {
$text = wfMsgNoTrans( 'permissionserrorstext', count($errors)). "\n\n";
} else {
global $wgLang;
- $action_desc = wfMsg( "action-$action" );
+ $action_desc = wfMsgNoTrans( "action-$action" );
$text = wfMsgNoTrans( 'permissionserrorstext-withaction', count($errors), $action_desc ) . "\n\n";
}
@@ -1250,7 +1849,7 @@ class OutputPage {
}
$text .= '</ul>';
} else {
- $text .= '<div class="permissions-errors">' . call_user_func_array( 'wfMsgNoTrans', reset( $errors ) ) . '</div>';
+ $text .= "<div class=\"permissions-errors\">\n" . call_user_func_array( 'wfMsgNoTrans', reset( $errors ) ) . "\n</div>";
}
return $text;
@@ -1271,12 +1870,13 @@ class OutputPage {
*
* @todo Needs to be split into multiple functions.
*
- * @param string $source Source code to show (or null).
- * @param bool $protected Is this a permissions error?
- * @param array $reasons List of reasons for this error, as returned by Title::getUserPermissionsErrors().
+ * @param $source String: source code to show (or null).
+ * @param $protected Boolean: is this a permissions error?
+ * @param $reasons Array: list of reasons for this error, as returned by Title::getUserPermissionsErrors().
+ * @param $action String: action that was denied or null if unknown
*/
public function readOnlyPage( $source = null, $protected = false, $reasons = array(), $action = null ) {
- global $wgUser, $wgTitle;
+ global $wgUser;
$skin = $wgUser->getSkin();
$this->setRobotPolicy( 'noindex,nofollow' );
@@ -1292,7 +1892,18 @@ class OutputPage {
// Permissions error
if( $source ) {
$this->setPageTitle( wfMsg( 'viewsource' ) );
- $this->setSubtitle( wfMsg( 'viewsourcefor', $skin->makeKnownLinkObj( $wgTitle ) ) );
+ $this->setSubtitle(
+ wfMsg(
+ 'viewsourcefor',
+ $skin->link(
+ $this->getTitle(),
+ null,
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ )
+ )
+ );
} else {
$this->setPageTitle( wfMsg( 'badaccess' ) );
}
@@ -1301,25 +1912,25 @@ class OutputPage {
// Wiki is read only
$this->setPageTitle( wfMsg( 'readonly' ) );
$reason = wfReadOnlyReason();
- $this->wrapWikiMsg( '<div class="mw-readonly-error">$1</div>', array( 'readonlytext', $reason ) );
+ $this->wrapWikiMsg( '<div class="mw-readonly-error">\n$1</div>', array( 'readonlytext', $reason ) );
}
// Show source, if supplied
if( is_string( $source ) ) {
$this->addWikiMsg( 'viewsourcetext' );
- $text = Xml::openElement( 'textarea',
- array( 'id' => 'wpTextbox1',
- 'name' => 'wpTextbox1',
- 'cols' => $wgUser->getOption( 'cols' ),
- 'rows' => $wgUser->getOption( 'rows' ),
- 'readonly' => 'readonly' ) );
- $text .= htmlspecialchars( $source );
- $text .= Xml::closeElement( 'textarea' );
- $this->addHTML( $text );
+
+ $params = array(
+ 'id' => 'wpTextbox1',
+ 'name' => 'wpTextbox1',
+ 'cols' => $wgUser->getOption( 'cols' ),
+ 'rows' => $wgUser->getOption( 'rows' ),
+ 'readonly' => 'readonly'
+ );
+ $this->addHTML( Html::element( 'textarea', $params, $source ) );
// Show templates used by this article
$skin = $wgUser->getSkin();
- $article = new Article( $wgTitle );
+ $article = new Article( $this->getTitle() );
$this->addHTML( "<div class='templatesUsed'>
{$skin->formatTemplates( $article->getUsedTemplates() )}
</div>
@@ -1329,12 +1940,23 @@ class OutputPage {
# If the title doesn't exist, it's fairly pointless to print a return
# 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( null, $wgTitle );
+ if( $this->getTitle()->exists() ) {
+ $this->returnToMain( null, $this->getTitle() );
}
}
/** @deprecated */
+ public function errorpage( $title, $msg ) {
+ wfDeprecated( __METHOD__ );
+ throw new ErrorPageError( $title, $msg );
+ }
+
+ /** @deprecated */
+ public function databaseError( $fname, $sql, $error, $errno ) {
+ throw new MWException( "OutputPage::databaseError is obsolete\n" );
+ }
+
+ /** @deprecated */
public function fatalError( $message ) {
wfDeprecated( __METHOD__ );
throw new FatalError( $message );
@@ -1402,30 +2024,37 @@ class OutputPage {
/**
* Add a "return to" link pointing to a specified title
*
- * @param Title $title Title to link
+ * @param $title Title to link
+ * @param $query String: query string
*/
- public function addReturnTo( $title ) {
+ public function addReturnTo( $title, $query = array() ) {
global $wgUser;
$this->addLink( array( 'rel' => 'next', 'href' => $title->getFullUrl() ) );
- $link = wfMsg( 'returnto', $wgUser->getSkin()->makeLinkObj( $title ) );
- $this->addHTML( "<p>{$link}</p>\n" );
+ $link = wfMsgHtml( 'returnto', $wgUser->getSkin()->link(
+ $title, null, array(), $query ) );
+ $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
}
/**
* Add a "return to" link pointing to a specified title,
* or the title indicated in the request, or else the main page
*
- * @param null $unused No longer used
- * @param Title $returnto Title to return to
+ * @param $unused No longer used
+ * @param $returnto Title or String to return to
+ * @param $returntoquery String: query string for the return to link
*/
- public function returnToMain( $unused = null, $returnto = NULL ) {
+ public function returnToMain( $unused = null, $returnto = null, $returntoquery = null ) {
global $wgRequest;
- if ( $returnto == NULL ) {
+ if ( $returnto == null ) {
$returnto = $wgRequest->getText( 'returnto' );
}
- if ( '' === $returnto ) {
+ if ( $returntoquery == null ) {
+ $returntoquery = $wgRequest->getText( 'returntoquery' );
+ }
+
+ if ( $returnto === '' ) {
$returnto = Title::newMainPage();
}
@@ -1438,43 +2067,24 @@ class OutputPage {
$titleObj = Title::newMainPage();
}
- $this->addReturnTo( $titleObj );
- }
-
- /**
- * This function takes the title (first item of mGoodLinks), categories, existing and broken links for the page
- * and uses the first 10 of them for META keywords
- *
- * @param ParserOutput &$parserOutput
- */
- private function addKeywords( &$parserOutput ) {
- global $wgTitle;
- $this->addKeyword( $wgTitle->getPrefixedText() );
- $count = 1;
- $links2d =& $parserOutput->getLinks();
- if ( !is_array( $links2d ) ) {
- return;
- }
- foreach ( $links2d as $dbkeys ) {
- foreach( $dbkeys as $dbkey => $unused ) {
- $this->addKeyword( $dbkey );
- if ( ++$count > 10 ) {
- break 2;
- }
- }
- }
+ $this->addReturnTo( $titleObj, $returntoquery );
}
/**
- * @return string The doctype, opening <html>, and head element.
+ * @param $sk Skin The given Skin
+ * @param $includeStyle Unused (?)
+ * @return String: The doctype, opening <html>, and head element.
*/
- public function headElement( Skin $sk ) {
+ public function headElement( Skin $sk, $includeStyle = true ) {
global $wgDocType, $wgDTD, $wgContLanguageCode, $wgOutputEncoding, $wgMimeType;
- global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces;
- global $wgUser, $wgContLang, $wgUseTrackbacks, $wgTitle, $wgStyleVersion;
+ global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces, $wgHtml5Version;
+ global $wgContLang, $wgUseTrackbacks, $wgStyleVersion, $wgHtml5, $wgWellFormedXml;
+ global $wgUser, $wgRequest, $wgLang;
- $this->addMeta( "http:Content-type", "$wgMimeType; charset={$wgOutputEncoding}" );
- $this->addStyle( 'common/wikiprintable.css', 'print' );
+ $this->addMeta( "http:Content-Type", "$wgMimeType; charset={$wgOutputEncoding}" );
+ if ( $sk->commonPrintStylesheet() ) {
+ $this->addStyle( 'common/wikiprintable.css', 'print' );
+ }
$sk->setupUserCss( $this );
$ret = '';
@@ -1483,42 +2093,154 @@ class OutputPage {
$ret .= "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?" . ">\n";
}
- $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n";
-
- if ( '' == $this->getHTMLTitle() ) {
+ if ( $this->getHTMLTitle() == '' ) {
$this->setHTMLTitle( wfMsg( 'pagetitle', $this->getPageTitle() ));
}
- $rtl = $wgContLang->isRTL() ? " dir='RTL'" : '';
- $ret .= "<html xmlns=\"{$wgXhtmlDefaultNamespace}\" ";
- foreach($wgXhtmlNamespaces as $tag => $ns) {
- $ret .= "xmlns:{$tag}=\"{$ns}\" ";
+ $dir = $wgContLang->getDir();
+
+ if ( $wgHtml5 ) {
+ if ( $wgWellFormedXml ) {
+ # Unknown elements and attributes are okay in XML, but unknown
+ # named entities are well-formedness errors and will break XML
+ # parsers. Thus we need a doctype that gives us appropriate
+ # entity definitions. The HTML5 spec permits four legacy
+ # doctypes as obsolete but conforming, so let's pick one of
+ # those, although it makes our pages look like XHTML1 Strict.
+ # Isn't compatibility great?
+ $ret .= "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n";
+ } else {
+ # Much saner.
+ $ret .= "<!doctype html>\n";
+ }
+ $ret .= "<html lang=\"$wgContLanguageCode\" dir=\"$dir\"";
+ if ( $wgHtml5Version ) $ret .= " version=\"$wgHtml5Version\"";
+ $ret .= ">\n";
+ } else {
+ $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\" \"$wgDTD\">\n";
+ $ret .= "<html xmlns=\"{$wgXhtmlDefaultNamespace}\" ";
+ foreach($wgXhtmlNamespaces as $tag => $ns) {
+ $ret .= "xmlns:{$tag}=\"{$ns}\" ";
+ }
+ $ret .= "lang=\"$wgContLanguageCode\" dir=\"$dir\">\n";
}
- $ret .= "xml:lang=\"$wgContLanguageCode\" lang=\"$wgContLanguageCode\" $rtl>\n";
- $ret .= "<head>\n<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n\t\t";
- $ret .= implode( "\t\t", array(
+
+ $ret .= "<head>\n";
+ $ret .= "<title>" . htmlspecialchars( $this->getHTMLTitle() ) . "</title>\n";
+ $ret .= implode( "\n", array(
$this->getHeadLinks(),
$this->buildCssLinks(),
- $sk->getHeadScripts( $this->mAllowUserJs ),
- $this->mScripts,
+ $this->getHeadScripts( $sk ),
$this->getHeadItems(),
));
if( $sk->usercss ){
- $ret .= "<style type='text/css'>{$sk->usercss}</style>";
+ $ret .= Html::inlineStyle( $sk->usercss );
}
if ($wgUseTrackbacks && $this->isArticleRelated())
- $ret .= $wgTitle->trackbackRDF();
+ $ret .= $this->getTitle()->trackbackRDF();
$ret .= "</head>\n";
+
+ $bodyAttrs = array();
+
+ # Crazy edit-on-double-click stuff
+ $action = $wgRequest->getVal( 'action', 'view' );
+
+ if ( $this->getTitle()->getNamespace() != NS_SPECIAL
+ && !in_array( $action, array( 'edit', 'submit' ) )
+ && $wgUser->getOption( 'editondblclick' ) ) {
+ $bodyAttrs['ondblclick'] = "document.location = '" . Xml::escapeJsString( $this->getTitle()->getEditURL() ) . "'";
+ }
+
+ # Class bloat
+ $bodyAttrs['class'] = "mediawiki $dir";
+
+ if ( $wgLang->capitalizeAllNouns() ) {
+ # A <body> class is probably not the best way to do this . . .
+ $bodyAttrs['class'] .= ' capitalize-all-nouns';
+ }
+ $bodyAttrs['class'] .= ' ns-' . $this->getTitle()->getNamespace();
+ if ( $this->getTitle()->getNamespace() == NS_SPECIAL ) {
+ $bodyAttrs['class'] .= ' ns-special';
+ } elseif ( $this->getTitle()->isTalkPage() ) {
+ $bodyAttrs['class'] .= ' ns-talk';
+ } else {
+ $bodyAttrs['class'] .= ' ns-subject';
+ }
+ $bodyAttrs['class'] .= ' ' . Sanitizer::escapeClass( 'page-' . $this->getTitle()->getPrefixedText() );
+ $bodyAttrs['class'] .= ' skin-' . Sanitizer::escapeClass( $wgUser->getSkin()->getSkinName() );
+
+ $ret .= Html::openElement( 'body', $bodyAttrs ) . "\n";
+
return $ret;
}
-
+
+ /**
+ * Gets the global variables and mScripts; also adds userjs to the end if
+ * enabled
+ *
+ * @param $sk Skin object to use
+ * @return String: HTML fragment
+ */
+ function getHeadScripts( Skin $sk ) {
+ global $wgUser, $wgRequest, $wgJsMimeType, $wgUseSiteJs;
+ global $wgStylePath, $wgStyleVersion;
+
+ $scripts = Skin::makeGlobalVariablesScript( $sk->getSkinName() );
+ $scripts .= Html::linkedScript( "{$wgStylePath}/common/wikibits.js?$wgStyleVersion" );
+
+ //add site JS if enabled:
+ if( $wgUseSiteJs ) {
+ $jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : '';
+ $this->addScriptFile( Skin::makeUrl( '-',
+ "action=raw$jsCache&gen=js&useskin=" .
+ urlencode( $sk->getSkinName() )
+ )
+ );
+ }
+
+ //add user js if enabled:
+ if( $this->isUserJsAllowed() && $wgUser->isLoggedIn() ) {
+ $action = $wgRequest->getVal( 'action', 'view' );
+ if( $this->mTitle && $this->mTitle->isJsSubpage() and $sk->userCanPreview( $action ) ) {
+ # XXX: additional security check/prompt?
+ $this->addInlineScript( $wgRequest->getText( 'wpTextbox1' ) );
+ } else {
+ $userpage = $wgUser->getUserPage();
+ $scriptpage = Title::makeTitleSafe(
+ NS_USER,
+ $userpage->getDBkey() . '/' . $sk->getSkinName() . '.js'
+ );
+ if ( $scriptpage && $scriptpage->exists() ) {
+ $userjs = Skin::makeUrl( $scriptpage->getPrefixedText(), 'action=raw&ctype=' . $wgJsMimeType );
+ $this->addScriptFile( $userjs );
+ }
+ }
+ }
+
+ $scripts .= "\n" . $this->mScripts;
+ return $scripts;
+ }
+
+ /**
+ * Add default \<meta\> tags
+ */
protected function addDefaultMeta() {
- global $wgVersion;
- $this->addMeta( 'http:Content-Style-Type', 'text/css' ); //bug 15835
+ global $wgVersion, $wgHtml5;
+
+ static $called = false;
+ if ( $called ) {
+ # Don't run this twice
+ return;
+ }
+ $called = true;
+
+ if ( !$wgHtml5 ) {
+ $this->addMeta( 'http:Content-Style-Type', 'text/css' ); //bug 15835
+ }
$this->addMeta( 'generator', "MediaWiki $wgVersion" );
-
+
$p = "{$this->mIndexPolicy},{$this->mFollowPolicy}";
if( $p !== 'index,follow' ) {
// http://www.robotstxt.org/wc/meta-user.html
@@ -1540,12 +2262,12 @@ class OutputPage {
*/
public function getHeadLinks() {
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';
@@ -1553,17 +2275,16 @@ class OutputPage {
} else {
$a = 'name';
}
- $tags[] = Xml::element( 'meta',
+ $tags[] = Html::element( 'meta',
array(
$a => $tag[0],
'content' => $tag[1] ) );
}
foreach ( $this->mLinktags as $tag ) {
- $tags[] = Xml::element( 'link', $tag );
+ $tags[] = Html::element( 'link', $tag );
}
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
@@ -1573,30 +2294,30 @@ class OutputPage {
$tags[] = $this->feedLink(
$format,
$link,
- wfMsg( "page-{$format}-feed", $wgTitle->getPrefixedText() ) ); # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
+ # Used messages: 'page-rss-feed' and 'page-atom-feed' (for an easier grep)
+ wfMsg( "page-{$format}-feed", $this->getTitle()->getPrefixedText() ) );
}
- # 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
+ # 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. Some sites might have a different feed they'd
# like to promote instead of the RC feed (maybe like a "Recent New Articles"
# or "Breaking news" one). For this, we see if $wgOverrideSiteFeed is defined.
# If so, use it instead.
-
- global $wgOverrideSiteFeed, $wgSitename, $wgFeedClasses;
+
+ global $wgOverrideSiteFeed, $wgSitename, $wgAdvertisedFeedTypes;
$rctitle = SpecialPage::getTitleFor( 'Recentchanges' );
-
+
if ( $wgOverrideSiteFeed ) {
- foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) {
+ foreach ( $wgOverrideSiteFeed as $type => $feedUrl ) {
$tags[] = $this->feedLink (
$type,
htmlspecialchars( $feedUrl ),
wfMsg( "site-{$type}-feed", $wgSitename ) );
}
- }
- else if ( $wgTitle->getPrefixedText() != $rctitle->getPrefixedText() ) {
- foreach( $wgFeedClasses as $format => $class ) {
+ } elseif ( $this->getTitle()->getPrefixedText() != $rctitle->getPrefixedText() ) {
+ foreach ( $wgAdvertisedFeedTypes as $format ) {
$tags[] = $this->feedLink(
$format,
$rctitle->getLocalURL( "feed={$format}" ),
@@ -1605,36 +2326,19 @@ class OutputPage {
}
}
- return implode( "\n\t\t", $tags ) . "\n";
- }
-
- /**
- * Return URLs for each supported syndication format for this page.
- * @return array associating format keys with URLs
- */
- public function getSyndicationLinks() {
- global $wgTitle, $wgFeedClasses;
- $links = array();
-
- if( $this->isSyndicated() ) {
- if( is_string( $this->getFeedAppendQuery() ) ) {
- $appendQuery = "&" . $this->getFeedAppendQuery();
- } else {
- $appendQuery = "";
- }
-
- foreach( $wgFeedClasses as $format => $class ) {
- $links[$format] = $wgTitle->getLocalUrl( "feed=$format{$appendQuery}" );
- }
- }
- return $links;
+ return implode( "\n", $tags );
}
/**
- * Generate a <link rel/> for an RSS feed.
+ * Generate a <link rel/> for a feed.
+ *
+ * @param $type String: feed type
+ * @param $url String: URL to the feed
+ * @param $text String: value of the "title" attribute
+ * @return String: HTML fragment
*/
private function feedLink( $type, $url, $text ) {
- return Xml::element( 'link', array(
+ return Html::element( 'link', array(
'rel' => 'alternate',
'type' => "application/$type+xml",
'title' => $text,
@@ -1645,12 +2349,15 @@ class OutputPage {
* Add a local or specified stylesheet, with the given media options.
* Meant primarily for internal use...
*
- * @param $media -- to specify a media type, 'screen', 'printable', 'handheld' or any.
- * @param $conditional -- for IE conditional comments, specifying an IE version
- * @param $dir -- set to 'rtl' or 'ltr' for direction-specific sheets
+ * @param $style String: URL to the file
+ * @param $media String: to specify a media type, 'screen', 'printable', 'handheld' or any.
+ * @param $condition String: for IE conditional comments, specifying an IE version
+ * @param $dir String: set to 'rtl' or 'ltr' for direction-specific sheets
*/
public function addStyle( $style, $media='', $condition='', $dir='' ) {
$options = array();
+ // Even though we expect the media type to be lowercase, but here we
+ // force it to lowercase to be safe.
if( $media )
$options['media'] = $media;
if( $condition )
@@ -1661,6 +2368,14 @@ class OutputPage {
}
/**
+ * Adds inline CSS styles
+ * @param $style_css Mixed: inline CSS
+ */
+ public function addInlineStyle( $style_css ){
+ $this->mScripts .= Html::inlineStyle( $style_css );
+ }
+
+ /**
* Build a set of <link>s for the stylesheets specified in the $this->styles array.
* These will be applied to various media & IE conditionals.
*/
@@ -1672,15 +2387,23 @@ class OutputPage {
$links[] = $link;
}
- return implode( "\n\t\t", $links );
+ return implode( "\n", $links );
}
+ /**
+ * Generate \<link\> tags for stylesheets
+ *
+ * @param $style String: URL to the file
+ * @param $options Array: option, can contain 'condition', 'dir', 'media'
+ * keys
+ * @return String: HTML fragment
+ */
protected function styleLink( $style, $options ) {
global $wgRequest;
if( isset( $options['dir'] ) ) {
global $wgContLang;
- $siteDir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
+ $siteDir = $wgContLang->getDir();
if( $siteDir != $options['dir'] )
return '';
}
@@ -1691,7 +2414,7 @@ class OutputPage {
return '';
}
} else {
- $media = '';
+ $media = 'all';
}
if( substr( $style, 0, 1 ) == '/' ||
@@ -1703,15 +2426,7 @@ class OutputPage {
$url = $wgStylePath . '/' . $style . '?' . $wgStyleVersion;
}
- $attribs = array(
- 'rel' => 'stylesheet',
- 'href' => $url,
- 'type' => 'text/css' );
- if( $media ) {
- $attribs['media'] = $media;
- }
-
- $link = Xml::element( 'link', $attribs );
+ $link = Html::linkedStyle( $url, $media );
if( isset( $options['condition'] ) ) {
$condition = htmlspecialchars( $options['condition'] );
@@ -1720,6 +2435,12 @@ class OutputPage {
return $link;
}
+ /**
+ * Transform "media" attribute based on request parameters
+ *
+ * @param $media String: current value of the "media" attribute
+ * @return String: modified value of the "media" attribute
+ */
function transformCssMedia( $media ) {
global $wgRequest, $wgHandheldForIPhone;
@@ -1758,8 +2479,6 @@ class OutputPage {
* for when rate limiting has triggered.
*/
public function rateLimited() {
- global $wgTitle;
-
$this->setPageTitle(wfMsg('actionthrottled'));
$this->setRobotPolicy( 'noindex,follow' );
$this->setArticleRelated( false );
@@ -1769,25 +2488,7 @@ class OutputPage {
$this->setStatusCode(503);
$this->addWikiMsg( 'actionthrottledtext' );
- $this->returnToMain( null, $wgTitle );
- }
-
- /**
- * Show an "add new section" link?
- *
- * @return bool
- */
- public function showNewSectionLink() {
- return $this->mNewSectionLink;
- }
-
- /**
- * Forcibly hide the new section link?
- *
- * @return bool
- */
- public function forceHideNewSectionLink() {
- return $this->mHideNewSectionLink;
+ $this->returnToMain( null, $this->getTitle() );
}
/**
@@ -1797,16 +2498,16 @@ class OutputPage {
* then the warning is a bit more obvious. If the lag is
* lower than $wgSlaveLagWarning, then no warning is shown.
*
- * @param int $lag Slave lag
+ * @param $lag Integer: slave lag
*/
public function showLagWarning( $lag ) {
- global $wgSlaveLagWarning, $wgSlaveLagCritical;
+ global $wgSlaveLagWarning, $wgSlaveLagCritical, $wgLang;
if( $lag >= $wgSlaveLagWarning ) {
$message = $lag < $wgSlaveLagCritical
? 'lag-warn-normal'
: 'lag-warn-high';
- $warning = wfMsgExt( $message, 'parse', $lag );
- $this->addHTML( "<div class=\"mw-{$message}\">\n{$warning}\n</div>\n" );
+ $wrap = Html::rawElement( 'div', array( 'class' => "mw-{$message}" ), "\n$1\n" );
+ $this->wrapWikiMsg( "$wrap\n", array( $message, $wgLang->formatNum( $lag ) ) );
}
}
@@ -1841,7 +2542,7 @@ class OutputPage {
*
* 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,
+ * message names, or 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
@@ -1851,11 +2552,13 @@ class OutputPage {
*
* For example:
*
- * $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'some-error' );
+ * $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>", 'some-error' );
*
* Is equivalent to:
*
- * $wgOut->addWikiText( '<div class="error">' . wfMsgNoTrans( 'some-error' ) . '</div>' );
+ * $wgOut->addWikiText( "<div class='error'>\n" . wfMsgNoTrans( 'some-error' ) . "</div>" );
+ *
+ * The newline after opening div is needed in some wikitext. See bug 19226.
*/
public function wrapWikiMsg( $wrap /*, ...*/ ) {
$msgSpecs = func_get_args();
@@ -1879,4 +2582,29 @@ class OutputPage {
}
$this->addHTML( $this->parse( $s, /*linestart*/true, /*uilang*/true ) );
}
+
+ /**
+ * Include jQuery core. Use this to avoid loading it multiple times
+ * before we get a usable script loader.
+ *
+ * @param $modules Array: list of jQuery modules which should be loaded
+ * @return Array: the list of modules which were not loaded.
+ */
+ public function includeJQuery( $modules = array() ) {
+ global $wgStylePath, $wgStyleVersion, $wgJsMimeType;
+
+ $supportedModules = array( /** TODO: add things here */ );
+ $unsupported = array_diff( $modules, $supportedModules );
+
+ $params = array(
+ 'type' => $wgJsMimeType,
+ 'src' => "$wgStylePath/common/jquery.min.js?$wgStyleVersion",
+ );
+ if ( !$this->mJQueryDone ) {
+ $this->mJQueryDone = true;
+ $this->mScripts = Html::element( 'script', $params ) . "\n" . $this->mScripts;
+ }
+ return $unsupported;
+ }
+
}
diff --git a/includes/PageHistory.php b/includes/PageHistory.php
deleted file mode 100644
index 9477981f..00000000
--- a/includes/PageHistory.php
+++ /dev/null
@@ -1,630 +0,0 @@
-<?php
-/**
- * Page history
- *
- * Split off from Article.php and Skin.php, 2003-12-22
- * @file
- */
-
-/**
- * This class handles printing the history page for an article. In order to
- * be efficient, it uses timestamps rather than offsets for paging, to avoid
- * costly LIMIT,offset queries.
- *
- * Construct it by passing in an Article, and call $h->history() to print the
- * history.
- *
- */
-class PageHistory {
- const DIR_PREV = 0;
- const DIR_NEXT = 1;
-
- var $mArticle, $mTitle, $mSkin;
- var $lastdate;
- var $linesonpage;
- var $mLatestId = null;
-
- private $mOldIdChecked = 0;
-
- /**
- * Construct a new PageHistory.
- *
- * @param Article $article
- * @returns nothing
- */
- function __construct( $article ) {
- global $wgUser;
- $this->mArticle =& $article;
- $this->mTitle =& $article->mTitle;
- $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') );
- }
- }
- }
-
- /**
- * Print the history page for an article.
- *
- * @returns nothing
- */
- function history() {
- global $wgOut, $wgRequest, $wgTitle, $wgScript;
-
- /*
- * Allow client caching.
- */
- if( $wgOut->checkLastModified( $this->mArticle->getTouched() ) )
- return; // Client cache fresh and headers sent, nothing more to do.
-
- wfProfileIn( __METHOD__ );
-
- /*
- * Setup page variables.
- */
- $wgOut->setPageTitle( wfMsg( 'history-title', $this->mTitle->getPrefixedText() ) );
- $wgOut->setPageTitleActionText( wfMsg( 'history_short' ) );
- $wgOut->setArticleFlag( false );
- $wgOut->setArticleRelated( true );
- $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() );
- $wgOut->setSubtitle( $logLink );
-
- $feedType = $wgRequest->getVal( 'feed' );
- if( $feedType ) {
- wfProfileOut( __METHOD__ );
- return $this->feed( $feedType );
- }
-
- /*
- * Fail if article doesn't exist.
- */
- if( !$this->mTitle->exists() ) {
- $wgOut->addWikiMsg( 'nohistory' );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- /**
- * Add date selector to quickly get to a certain time
- */
- $year = $wgRequest->getInt( 'year' );
- $month = $wgRequest->getInt( 'month' );
- $tagFilter = $wgRequest->getVal( 'tagfilter' );
- $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter );
-
- $action = htmlspecialchars( $wgScript );
- $wgOut->addHTML(
- "<form action=\"$action\" method=\"get\" id=\"mw-history-searchform\">" .
- Xml::fieldset( wfMsg( 'history-fieldset-title' ), false, array( 'id' => 'mw-history-search' ) ) .
- Xml::hidden( 'title', $this->mTitle->getPrefixedDBKey() ) . "\n" .
- Xml::hidden( 'action', 'history' ) . "\n" .
- xml::dateMenu( $year, $month ) . '&nbsp;' .
- ( $tagSelector ? ( implode( '&nbsp;', $tagSelector ) . '&nbsp;' ) : '' ) .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
- '</fieldset></form>'
- );
-
- wfRunHooks( 'PageHistoryBeforeList', array( &$this->mArticle ) );
-
- /**
- * Do the list
- */
- $pager = new PageHistoryPager( $this, $year, $month, $tagFilter );
- $this->linesonpage = $pager->getNumRows();
- $wgOut->addHTML(
- $pager->getNavigationBar() .
- $this->beginHistoryList() .
- $pager->getBody() .
- $this->endHistoryList() .
- $pager->getNavigationBar()
- );
-
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Creates begin of history list with a submit button
- *
- * @return string HTML output
- */
- function beginHistoryList() {
- global $wgTitle, $wgScript, $wgEnableHtmlDiff;
- $this->lastdate = '';
- $s = wfMsgExt( 'histlegend', array( 'parse') );
- $s .= Xml::openElement( 'form', array( 'action' => $wgScript, 'id' => 'mw-history-compare' ) );
- $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
- if( $wgEnableHtmlDiff ) {
- $s .= $this->submitButton( wfMsg( 'visualcomparison'),
- array(
- 'name' => 'htmldiff',
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-visualcomparison' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
- $s .= $this->submitButton( wfMsg( 'wikicodecomparison'),
- array(
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
- } else {
- $s .= $this->submitButton( wfMsg( 'compareselectedversions'),
- array(
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
- }
- $s .= '<ul id="pagehistory">' . "\n";
- return $s;
- }
-
- /**
- * Creates end of history list with a submit button
- *
- * @return string HTML output
- */
- function endHistoryList() {
- global $wgEnableHtmlDiff;
- $s = '</ul>';
- if( $wgEnableHtmlDiff ) {
- $s .= $this->submitButton( wfMsg( 'visualcomparison'),
- array(
- 'name' => 'htmldiff',
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-visualcomparison' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
- $s .= $this->submitButton( wfMsg( 'wikicodecomparison'),
- array(
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
- } else {
- $s .= $this->submitButton( wfMsg( 'compareselectedversions'),
- array(
- 'class' => 'historysubmit',
- 'accesskey' => wfMsg( 'accesskey-compareselectedversions' ),
- 'title' => wfMsg( 'tooltip-compareselectedversions' ),
- )
- );
- }
- $s .= '</form>';
- return $s;
- }
-
- /**
- * Creates a submit button
- *
- * @param array $attributes attributes
- * @return string HTML output for the submit button
- */
- function submitButton($message, $attributes = array() ) {
- # Disable submit button if history has 1 revision only
- if( $this->linesonpage > 1 ) {
- return Xml::submitButton( $message , $attributes );
- } else {
- return '';
- }
- }
-
- /**
- * Returns a row from the history printout.
- *
- * @todo document some more, and maybe clean up the code (some params redundant?)
- *
- * @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.
- * @param bool $firstInList Whether this row corresponds to the first displayed on this history page.
- * @return string HTML output for the row
- */
- function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) {
- global $wgUser, $wgLang;
- $rev = new Revision( $row );
- $rev->setTitle( $this->mTitle );
-
- $curlink = $this->curLink( $rev, $latest );
- $lastlink = $this->lastLink( $rev, $next, $counter );
- $arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
- $link = $this->revLink( $rev );
- $classes = array();
-
- $s = "($curlink) ($lastlink) $arbitrary";
-
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
- if( $latest ) {
- // We don't currently handle well changing the top revision's settings
- $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' );
- } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' );
- } else {
- $query = array( 'target' => $this->mTitle->getPrefixedDbkey(),
- 'oldid' => $rev->getId()
- );
- $del = $this->mSkin->revDeleteLink( $query, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
- }
- $s .= " $del ";
- }
-
- $s .= " $link";
- $s .= " <span class='history-user'>" . $this->mSkin->revUserTools( $rev, true ) . "</span>";
-
- if( $rev->isMinor() ) {
- $s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') );
- }
-
- if( !is_null( $size = $rev->getSize() ) && !$rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $s .= ' ' . $this->mSkin->formatRevisionSize( $size );
- }
-
- $s .= $this->mSkin->revComment( $rev, false, true );
-
- if( $notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp) ) {
- $s .= ' <span class="updatedmarker">' . wfMsgHtml( 'updatedmarker' ) . '</span>';
- }
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
- }
-
- $tools = array();
-
- if( !is_null( $next ) && is_object( $next ) ) {
- if( $latest && $this->mTitle->userCan( 'rollback' ) && $this->mTitle->userCan( 'edit' ) ) {
- $tools[] = '<span class="mw-rollback-link">'.$this->mSkin->buildRollbackLink( $rev ).'</span>';
- }
-
- if( $this->mTitle->quickUserCan( 'edit' ) && !$rev->isDeleted( Revision::DELETED_TEXT ) &&
- !$next->rev_deleted & Revision::DELETED_TEXT )
- {
- # Create undo tooltip for the first (=latest) line only
- $undoTooltip = $latest
- ? array( 'title' => wfMsg( 'tooltip-undo' ) )
- : array();
- $undolink = $this->mSkin->link(
- $this->mTitle,
- wfMsgHtml( 'editundo' ),
- $undoTooltip,
- array( 'action' => 'edit', 'undoafter' => $next->rev_id, 'undo' => $rev->getId() ),
- array( 'known', 'noclasses' )
- );
- $tools[] = "<span class=\"mw-history-undo\">{$undolink}</span>";
- }
- }
-
- if( $tools ) {
- $s .= ' (' . $wgLang->pipeList( $tools ) . ')';
- }
-
- # Tags
- list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' );
- $classes = array_merge( $classes, $newClasses );
- $s .= " $tagSummary";
-
- wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s ) );
-
- $classes = implode( ' ', $classes );
-
- return "<li class=\"$classes\">$s</li>\n";
- }
-
- /**
- * Create a link to view this revision of the page
- * @param Revision $rev
- * @returns string
- */
- function revLink( $rev ) {
- global $wgLang;
- $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
- if( !$rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $link = $this->mSkin->makeKnownLinkObj( $this->mTitle, $date, "oldid=" . $rev->getId() );
- } else {
- $link = '<span class="history-deleted">' . $date . '</span>';
- }
- return $link;
- }
-
- /**
- * Create a diff-to-current link for this revision for this page
- * @param Revision $rev
- * @param Bool $latest, this is the latest revision of the page?
- * @returns string
- */
- function curLink( $rev, $latest ) {
- $cur = $this->message['cur'];
- if( $latest || $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- return $cur;
- } else {
- return $this->mSkin->makeKnownLinkObj( $this->mTitle, $cur,
- 'diff=' . $this->mTitle->getLatestRevID() . "&oldid=" . $rev->getId() );
- }
- }
-
- /**
- * Create a diff-to-previous link for this revision for this page.
- * @param Revision $prevRev, the previous revision
- * @param mixed $next, the newer revision
- * @param int $counter, what row on the history list this is
- * @returns string
- */
- 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' ) {
- # Next row probably exists but is unknown, use an oldid=prev link
- return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last,
- "diff=" . $prevRev->getId() . "&oldid=prev" );
- } elseif( $prevRev->isDeleted(Revision::DELETED_TEXT) || $nextRev->isDeleted(Revision::DELETED_TEXT) ) {
- return $last;
- } else {
- return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last,
- "diff=" . $prevRev->getId() . "&oldid={$next->rev_id}" );
- }
- }
-
- /**
- * Create radio buttons for page history
- *
- * @param object $rev Revision
- * @param bool $firstInList Is this version the first one?
- * @param int $counter A counter of what row number we're at, counted from the top row = 1.
- * @return string HTML output for the radio buttons
- */
- function diffButtons( $rev, $firstInList, $counter ) {
- if( $this->linesonpage > 1 ) {
- $radio = array( 'type' => 'radio', 'value' => $rev->getId() );
- /** @todo: move title texts to javascript */
- if( $firstInList ) {
- $first = Xml::element( 'input',
- array_merge( $radio, array( 'style' => 'visibility:hidden', 'name' => 'oldid' ) )
- );
- $checkmark = array( 'checked' => 'checked' );
- } else {
- # Check visibility of old revisions
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $radio['disabled'] = 'disabled';
- $checkmark = array(); // We will check the next possible one
- } else if( $counter == 2 || !$this->mOldIdChecked ) {
- $checkmark = array( 'checked' => 'checked' );
- $this->mOldIdChecked = $rev->getId();
- } else {
- $checkmark = array();
- }
- $first = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'oldid' ) ) );
- $checkmark = array();
- }
- $second = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'diff' ) ) );
- return $first . $second;
- } else {
- return '';
- }
- }
-
- /**
- * Fetch an array of revisions, specified by a given limit, offset and
- * direction. This is now only used by the feeds. It was previously
- * used by the main UI but that's now handled by the pager.
- */
- function fetchRevisions($limit, $offset, $direction) {
- $dbr = wfGetDB( DB_SLAVE );
-
- if( $direction == PageHistory::DIR_PREV )
- list($dirs, $oper) = array("ASC", ">=");
- else /* $direction == PageHistory::DIR_NEXT */
- list($dirs, $oper) = array("DESC", "<=");
-
- if( $offset )
- $offsets = array("rev_timestamp $oper '$offset'");
- else
- $offsets = array();
-
- $page_id = $this->mTitle->getArticleID();
-
- return $dbr->select( 'revision',
- Revision::selectFields(),
- array_merge(array("rev_page=$page_id"), $offsets),
- __METHOD__,
- array( 'ORDER BY' => "rev_timestamp $dirs",
- 'USE INDEX' => 'page_timestamp', 'LIMIT' => $limit)
- );
- }
-
- /**
- * Output a subscription feed listing recent edits to this page.
- * @param string $type
- */
- function feed( $type ) {
- 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' ) );
-
- // 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 ) {
- $feed->outItem( $this->feedItem( $row ) );
- }
- } else {
- $feed->outItem( $this->feedEmpty() );
- }
- $feed->outFooter();
- }
-
- function feedEmpty() {
- global $wgOut;
- return new FeedItem(
- wfMsgForContent( 'nohistory' ),
- $wgOut->parse( wfMsgForContent( 'history-feed-empty' ) ),
- $this->mTitle->getFullUrl(),
- wfTimestamp( TS_MW ),
- '',
- $this->mTitle->getTalkPage()->getFullUrl() );
- }
-
- /**
- * Generate a FeedItem object from a given revision table row
- * Borrows Recent Changes' feed generation functions for formatting;
- * includes a diff to the previous revision (if any).
- *
- * @param $row
- * @return FeedItem
- */
- function feedItem( $row ) {
- $rev = new Revision( $row );
- $rev->setTitle( $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',
- $rev->getUserText(),
- $wgContLang->timeanddate( $rev->getTimestamp() ) );
- } else {
- $title = $rev->getUserText() . wfMsgForContent( 'colon-separator' ) . FeedItem::stripComment( $rev->getComment() );
- }
-
- return new FeedItem(
- $title,
- $text,
- $this->mTitle->getFullUrl( 'diff=' . $rev->getId() . '&oldid=prev' ),
- $rev->getTimestamp(),
- $rev->getUserText(),
- $this->mTitle->getTalkPage()->getFullUrl() );
- }
-}
-
-
-/**
- * @ingroup Pager
- */
-class PageHistoryPager extends ReverseChronologicalPager {
- public $mLastRow = false, $mPageHistory, $mTitle;
-
- function __construct( $pageHistory, $year='', $month='', $tagFilter = '' ) {
- parent::__construct();
- $this->mPageHistory = $pageHistory;
- $this->mTitle =& $this->mPageHistory->mTitle;
- $this->tagFilter = $tagFilter;
- $this->getDateCond( $year, $month );
- }
-
- function getQueryInfo() {
- $queryInfo = array(
- 'tables' => array('revision'),
- 'fields' => array_merge( Revision::selectFields(), array('ts_tags') ),
- 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ),
- 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ),
- 'join_conds' => array( 'tag_summary' => array( 'LEFT JOIN', 'ts_rev_id=rev_id' ) ),
- );
- ChangeTags::modifyDisplayQuery( $queryInfo['tables'],
- $queryInfo['fields'],
- $queryInfo['conds'],
- $queryInfo['join_conds'],
- $queryInfo['options'],
- $this->tagFilter );
- wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) );
- return $queryInfo;
- }
-
- function getIndexField() {
- return 'rev_timestamp';
- }
-
- function formatRow( $row ) {
- if( $this->mLastRow ) {
- $latest = $this->mCounter == 1 && $this->mIsFirst;
- $firstInList = $this->mCounter == 1;
- $s = $this->mPageHistory->historyLine( $this->mLastRow, $row, $this->mCounter++,
- $this->mTitle->getNotificationTimestamp(), $latest, $firstInList );
- } else {
- $s = '';
- }
- $this->mLastRow = $row;
- return $s;
- }
-
- function getStartBody() {
- $this->mLastRow = false;
- $this->mCounter = 1;
- return '';
- }
-
- function getEndBody() {
- if( $this->mLastRow ) {
- $latest = $this->mCounter == 1 && $this->mIsFirst;
- $firstInList = $this->mCounter == 1;
- if( $this->mIsBackwards ) {
- # Next row is unknown, but for UI reasons, probably exists if an offset has been specified
- if( $this->mOffset == '' ) {
- $next = null;
- } else {
- $next = 'unknown';
- }
- } else {
- # The next row is the past-the-end row
- $next = $this->mPastTheEndRow;
- }
- $s = $this->mPageHistory->historyLine( $this->mLastRow, $next, $this->mCounter++,
- $this->mTitle->getNotificationTimestamp(), $latest, $firstInList );
- } else {
- $s = '';
- }
- return $s;
- }
-}
diff --git a/includes/Pager.php b/includes/Pager.php
index 8faec533..e5eef1fc 100644
--- a/includes/Pager.php
+++ b/includes/Pager.php
@@ -50,7 +50,7 @@ interface Pager {
* 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 HistoryPage.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.
@@ -67,11 +67,12 @@ abstract class IndexPager implements Pager {
public $mPastTheEndRow;
/**
- * The index to actually be used for ordering. This is a single string e-
- * ven if multiple orderings are supported.
+ * The index to actually be used for ordering. This is a single string
+ * even if multiple orderings are supported.
*/
protected $mIndexField;
- /** For pages that support multiple types of ordering, which one to use. */
+ /** For pages that support multiple types of ordering, which one to use.
+ */
protected $mOrderType;
/**
* $mDefaultDirection gives the direction to use when sorting results:
@@ -87,6 +88,9 @@ abstract class IndexPager implements Pager {
public $mDefaultDirection;
public $mIsBackwards;
+ /** True if the current result set is the first one */
+ public $mIsFirst;
+
/**
* Result object for the query. Warning: seek before use.
*/
@@ -145,7 +149,11 @@ abstract class IndexPager implements Pager {
# Plus an extra row so that we can tell the "next" link should be shown
$queryLimit = $this->mLimit + 1;
- $this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending );
+ $this->mResult = $this->reallyDoQuery(
+ $this->mOffset,
+ $queryLimit,
+ $descending
+ );
$this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult );
$this->mQueryDone = true;
@@ -154,14 +162,14 @@ abstract class IndexPager implements Pager {
wfProfileOut( $fname );
}
-
+
/**
* Return the result wrapper.
*/
function getResult() {
return $this->mResult;
}
-
+
/**
* Set the offset from an other source than $wgRequest
*/
@@ -226,6 +234,13 @@ abstract class IndexPager implements Pager {
}
/**
+ * Get some text to go in brackets in the "function name" part of the SQL comment
+ */
+ function getSqlComment() {
+ return get_class( $this );
+ }
+
+ /**
* Do a query with specified parameters, rather than using the object
* context
*
@@ -235,7 +250,7 @@ abstract class IndexPager implements Pager {
* @return ResultWrapper
*/
function reallyDoQuery( $offset, $limit, $descending ) {
- $fname = __METHOD__ . ' (' . get_class( $this ) . ')';
+ $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
$info = $this->getQueryInfo();
$tables = $info['tables'];
$fields = $info['fields'];
@@ -314,8 +329,13 @@ abstract class IndexPager implements Pager {
if( $type ) {
$attrs['class'] = "mw-{$type}link";
}
- return $this->getSkin()->link( $this->getTitle(), $text,
- $attrs, $query + $this->getDefaultQuery(), array('noclasses','known') );
+ return $this->getSkin()->link(
+ $this->getTitle(),
+ $text,
+ $attrs,
+ $query + $this->getDefaultQuery(),
+ array( 'noclasses', 'known' )
+ );
}
/**
@@ -404,7 +424,11 @@ abstract class IndexPager implements Pager {
$prev = false;
$first = false;
} else {
- $prev = array( 'dir' => 'prev', 'offset' => $this->mFirstShown, 'limit' => $urlLimit );
+ $prev = array(
+ 'dir' => 'prev',
+ 'offset' => $this->mFirstShown,
+ 'limit' => $urlLimit
+ );
$first = array( 'limit' => $urlLimit );
}
if ( $this->mIsLast ) {
@@ -414,7 +438,20 @@ abstract class IndexPager implements Pager {
$next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit );
$last = array( 'dir' => 'prev', 'limit' => $urlLimit );
}
- return array( 'prev' => $prev, 'next' => $next, 'first' => $first, 'last' => $last );
+ return array(
+ 'prev' => $prev,
+ 'next' => $next,
+ 'first' => $first,
+ 'last' => $last
+ );
+ }
+
+ function isNavigationBarShown() {
+ if ( !$this->mQueryDone ) {
+ $this->doQuery();
+ }
+ // Hide navigation by default if there is nothing to page
+ return !($this->mIsFirst && $this->mIsLast);
}
/**
@@ -428,7 +465,11 @@ abstract class IndexPager implements Pager {
$links = array();
foreach ( $queries as $type => $query ) {
if ( $query !== false ) {
- $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type], $type );
+ $links[$type] = $this->makeLink(
+ $linkTexts[$type],
+ $queries[$type],
+ $type
+ );
} elseif ( isset( $disabledTexts[$type] ) ) {
$links[$type] = $disabledTexts[$type];
} else {
@@ -447,8 +488,11 @@ abstract class IndexPager implements Pager {
$offset = $this->mOffset;
}
foreach ( $this->mLimitsShown as $limit ) {
- $links[] = $this->makeLink( $wgLang->formatNum( $limit ),
- array( 'offset' => $offset, 'limit' => $limit ), 'num' );
+ $links[] = $this->makeLink(
+ $wgLang->formatNum( $limit ),
+ array( 'offset' => $offset, 'limit' => $limit ),
+ 'num'
+ );
}
return $links;
}
@@ -468,6 +512,7 @@ abstract class IndexPager implements Pager {
* fields => Field(s) for passing to Database::select(), may be *
* conds => WHERE conditions
* options => option array
+ * join_conds => JOIN conditions
*/
abstract function getQueryInfo();
@@ -516,14 +561,24 @@ abstract class AlphabeticPager extends IndexPager {
function getNavigationBar() {
global $wgLang;
+ if ( !$this->isNavigationBarShown() ) return '';
+
if( isset( $this->mNavigationBar ) ) {
return $this->mNavigationBar;
}
$opts = array( 'parsemag', 'escapenoentities' );
$linkTexts = array(
- 'prev' => wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $this->mLimit ) ),
- 'next' => wfMsgExt( 'nextn', $opts, $wgLang->formatNum($this->mLimit ) ),
+ '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 )
);
@@ -533,7 +588,10 @@ abstract class AlphabeticPager extends IndexPager {
$limits = $wgLang->pipeList( $limitLinks );
$this->mNavigationBar =
- "(" . $wgLang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " .
+ "(" . $wgLang->pipeList(
+ array( $pagingLinks['first'],
+ $pagingLinks['last'] )
+ ) . ") " .
wfMsgHtml( 'viewprevnext', $pagingLinks['prev'],
$pagingLinks['next'], $limits );
@@ -597,13 +655,23 @@ abstract class ReverseChronologicalPager extends IndexPager {
function getNavigationBar() {
global $wgLang;
+ if ( !$this->isNavigationBarShown() ) return '';
+
if ( isset( $this->mNavigationBar ) ) {
return $this->mNavigationBar;
}
$nicenumber = $wgLang->formatNum( $this->mLimit );
$linkTexts = array(
- 'prev' => wfMsgExt( 'pager-newer-n', array( 'parsemag' ), $nicenumber ),
- 'next' => wfMsgExt( 'pager-older-n', array( 'parsemag' ), $nicenumber ),
+ 'prev' => wfMsgExt(
+ 'pager-newer-n',
+ array( 'parsemag', 'escape' ),
+ $nicenumber
+ ),
+ 'next' => wfMsgExt(
+ 'pager-older-n',
+ array( 'parsemag', 'escape' ),
+ $nicenumber
+ ),
'first' => wfMsgHtml( 'histlast' ),
'last' => wfMsgHtml( 'histfirst' )
);
@@ -612,11 +680,17 @@ abstract class ReverseChronologicalPager extends IndexPager {
$limitLinks = $this->getLimitLinks();
$limits = $wgLang->pipeList( $limitLinks );
- $this->mNavigationBar = "({$pagingLinks['first']}" . wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "{$pagingLinks['last']}) " .
- wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits);
+ $this->mNavigationBar = "({$pagingLinks['first']}" .
+ wfMsgExt( 'pipe-separator' , 'escapenoentities' ) .
+ "{$pagingLinks['last']}) " .
+ wfMsgHTML(
+ 'viewprevnext',
+ $pagingLinks['prev'], $pagingLinks['next'],
+ $limits
+ );
return $this->mNavigationBar;
}
-
+
function getDateCond( $year, $month ) {
$year = intval($year);
$month = intval($month);
@@ -745,27 +819,50 @@ abstract class TablePager extends IndexPager {
}
function formatRow( $row ) {
- $rowClass = $this->getRowClass( $row );
- $s = "<tr class=\"$rowClass\">\n";
+ $this->mCurrentRow = $row; # In case formatValue etc need to know
+ $s = Xml::openElement( 'tr', $this->getRowAttrs($row) );
$fieldNames = $this->getFieldNames();
- $this->mCurrentRow = $row; # In case formatValue needs to know
foreach ( $fieldNames as $field => $name ) {
$value = isset( $row->$field ) ? $row->$field : null;
$formatted = strval( $this->formatValue( $field, $value ) );
if ( $formatted == '' ) {
$formatted = '&nbsp;';
}
- $class = 'TablePager_col_' . htmlspecialchars( $field );
- $s .= "<td class=\"$class\">$formatted</td>\n";
+ $s .= Xml::tags( 'td', $this->getCellAttrs( $field, $value ), $formatted );
}
$s .= "</tr>\n";
return $s;
}
- function getRowClass($row) {
+ /**
+ * Get a class name to be applied to the given row.
+ * @param object $row The database result row
+ */
+ function getRowClass( $row ) {
return '';
}
+ /**
+ * Get attributes to be applied to the given row.
+ * @param object $row The database result row
+ * @return associative array
+ */
+ function getRowAttrs( $row ) {
+ return array( 'class' => $this->getRowClass( $row ) );
+ }
+
+ /**
+ * Get any extra attributes to be applied to the given cell. Don't
+ * take this as an excuse to hardcode styles; use classes and
+ * CSS instead. Row context is available in $this->mCurrentRow
+ * @param $field The column
+ * @param $value The cell contents
+ * @return associative array
+ */
+ function getCellAttrs( $field, $value ) {
+ return array( 'class' => 'TablePager_col_' . $field );
+ }
+
function getIndexField() {
return $this->mSort;
}
@@ -787,6 +884,9 @@ abstract class TablePager extends IndexPager {
*/
function getNavigationBar() {
global $wgStylePath, $wgContLang;
+
+ if ( !$this->isNavigationBarShown() ) return '';
+
$path = "$wgStylePath/common/images";
$labels = array(
'first' => 'table_pager_first',
@@ -795,24 +895,29 @@ abstract class TablePager extends IndexPager {
'last' => 'table_pager_last',
);
$images = array(
- 'first' => $wgContLang->isRTL() ? 'arrow_last_25.png' : 'arrow_first_25.png',
- 'prev' => $wgContLang->isRTL() ? 'arrow_right_25.png' : 'arrow_left_25.png',
- 'next' => $wgContLang->isRTL() ? 'arrow_left_25.png' : 'arrow_right_25.png',
- 'last' => $wgContLang->isRTL() ? 'arrow_first_25.png' : 'arrow_last_25.png',
+ 'first' => 'arrow_first_25.png',
+ 'prev' => 'arrow_left_25.png',
+ 'next' => 'arrow_right_25.png',
+ 'last' => 'arrow_last_25.png',
);
$disabledImages = array(
- 'first' => $wgContLang->isRTL() ? 'arrow_disabled_last_25.png' : 'arrow_disabled_first_25.png',
- 'prev' => $wgContLang->isRTL() ? 'arrow_disabled_right_25.png' : 'arrow_disabled_left_25.png',
- 'next' => $wgContLang->isRTL() ? 'arrow_disabled_left_25.png' : 'arrow_disabled_right_25.png',
- 'last' => $wgContLang->isRTL() ? 'arrow_disabled_first_25.png' : 'arrow_disabled_last_25.png',
+ 'first' => 'arrow_disabled_first_25.png',
+ 'prev' => 'arrow_disabled_left_25.png',
+ 'next' => 'arrow_disabled_right_25.png',
+ 'last' => 'arrow_disabled_last_25.png',
);
+ if( $wgContLang->isRTL() ) {
+ $keys = array_keys( $labels );
+ $images = array_combine( $keys, array_reverse( $images ) );
+ $disabledImages = array_combine( $keys, array_reverse( $disabledImages ) );
+ }
$linkTexts = array();
$disabledTexts = array();
foreach ( $labels as $type => $label ) {
$msgLabel = wfMsgHtml( $label );
- $linkTexts[$type] = "<img src=\"$path/{$images[$type]}\" alt=\"$msgLabel\"/><br/>$msgLabel";
- $disabledTexts[$type] = "<img src=\"$path/{$disabledImages[$type]}\" alt=\"$msgLabel\"/><br/>$msgLabel";
+ $linkTexts[$type] = "<img src=\"$path/{$images[$type]}\" alt=\"$msgLabel\"/><br />$msgLabel";
+ $disabledTexts[$type] = "<img src=\"$path/{$disabledImages[$type]}\" alt=\"$msgLabel\"/><br />$msgLabel";
}
$links = $this->getPagingLinks( $linkTexts, $disabledTexts );
@@ -832,10 +937,19 @@ abstract class TablePager extends IndexPager {
function getLimitSelect() {
global $wgLang;
$s = "<select name=\"limit\">";
- foreach ( $this->mLimitsShown as $limit ) {
- $selected = $limit == $this->mLimit ? 'selected="selected"' : '';
- $formattedLimit = $wgLang->formatNum( $limit );
- $s .= "<option value=\"$limit\" $selected>$formattedLimit</option>\n";
+ foreach ( $this->mLimitsShown as $key => $value ) {
+ # The pair is either $index => $limit, in which case the $value
+ # will be numeric, or $limit => $text, in which case the $value
+ # will be a string.
+ if( is_int( $value ) ){
+ $limit = $value;
+ $text = $wgLang->formatNum( $limit );
+ } else {
+ $limit = $key;
+ $text = $value;
+ }
+ $selected = ( $limit == $this->mLimit ? 'selected="selected"' : '' );
+ $s .= "<option value=\"$limit\" $selected>$text</option>\n";
}
$s .= "</select>";
return $s;
@@ -865,14 +979,21 @@ abstract class TablePager extends IndexPager {
* Get a form containing a limit selection dropdown
*/
function getLimitForm() {
+ global $wgScript;
+
# Make the select with some explanatory text
- $url = $this->getTitle()->escapeLocalURL();
$msgSubmit = wfMsgHtml( 'table_pager_limit_submit' );
return
- "<form method=\"get\" action=\"$url\">" .
+ Xml::openElement(
+ 'form',
+ array(
+ 'method' => 'get',
+ 'action' => $wgScript
+ )
+ ) . "\n" .
wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) .
"\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
- $this->getHiddenFields( array('limit','title') ) .
+ $this->getHiddenFields( array( 'limit' ) ) .
"</form>\n";
}
diff --git a/includes/PatrolLog.php b/includes/PatrolLog.php
index 978821c1..5727853e 100644
--- a/includes/PatrolLog.php
+++ b/includes/PatrolLog.php
@@ -11,8 +11,8 @@ class PatrolLog {
/**
* Record a log event for a change being patrolled
*
- * @param mixed $change Change identifier or RecentChange object
- * @param bool $auto Was this patrol event automatic?
+ * @param $rc Mixed: change identifier or RecentChange object
+ * @param $auto Boolean: was this patrol event automatic?
*/
public static function record( $rc, $auto = false ) {
if( !( $rc instanceof RecentChange ) ) {
@@ -33,22 +33,30 @@ class PatrolLog {
/**
* Generate the log action text corresponding to a patrol log item
*
- * @param Title $title Title of the page that was patrolled
- * @param array $params Log parameters (from logging.log_params)
- * @param Skin $skin Skin to use for building links, etc.
- * @return string
+ * @param $title Title of the page that was patrolled
+ * @param $params Array: log parameters (from logging.log_params)
+ * @param $skin Skin to use for building links, etc.
+ * @return String
*/
public static function makeActionText( $title, $params, $skin ) {
list( $cur, /* $prev */, $auto ) = $params;
if( is_object( $skin ) ) {
# Standard link to the page in question
- $link = $skin->makeLinkObj( $title );
+ $link = $skin->link( $title );
if( $title->exists() ) {
# Generate a diff link
- $bits[] = 'oldid=' . urlencode( $cur );
- $bits[] = 'diff=prev';
- $bits = implode( '&', $bits );
- $diff = $skin->makeKnownLinkObj( $title, htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ), $bits );
+ $query = array(
+ 'oldid' => $cur,
+ 'diff' => 'prev'
+ );
+
+ $diff = $skin->link(
+ $title,
+ htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) ),
+ array(),
+ $query,
+ array( 'known', 'noclasses' )
+ );
} else {
# Don't bother with a diff link, it's useless
$diff = htmlspecialchars( wfMsg( 'patrol-log-diff', $cur ) );
@@ -66,9 +74,9 @@ class PatrolLog {
/**
* Prepare log parameters for a patrolled change
*
- * @param RecentChange $change RecentChange to represent
- * @param bool $auto Whether the patrol event was automatic
- * @return array
+ * @param $change RecentChange to represent
+ * @param $auto Boolean: whether the patrol event was automatic
+ * @return Array
*/
private static function buildParams( $change, $auto ) {
return array(
diff --git a/includes/PoolCounter.php b/includes/PoolCounter.php
new file mode 100644
index 00000000..2564fbc6
--- /dev/null
+++ b/includes/PoolCounter.php
@@ -0,0 +1,64 @@
+<?php
+
+abstract class PoolCounter {
+ public static function factory( $type, $key ) {
+ global $wgPoolCounterConf;
+ if ( !isset( $wgPoolCounterConf[$type] ) ) {
+ return new PoolCounter_Stub;
+ }
+ $conf = $wgPoolCounterConf[$type];
+ $class = $conf['class'];
+ return new $class( $conf, $type, $key );
+ }
+
+ abstract public function acquire();
+ abstract public function release();
+ abstract public function wait();
+
+ public function executeProtected( $mainCallback, $dirtyCallback = false ) {
+ $status = $this->acquire();
+ if ( !$status->isOK() ) {
+ return $status;
+ }
+ if ( !empty( $status->value['overload'] ) ) {
+ # Overloaded. Try a dirty cache entry.
+ if ( $dirtyCallback ) {
+ if ( call_user_func( $dirtyCallback ) ) {
+ $this->release();
+ return Status::newGood();
+ }
+ }
+
+ # Wait for a thread
+ $status = $this->wait();
+ if ( !$status->isOK() ) {
+ $this->release();
+ return $status;
+ }
+ }
+ # Call the main callback
+ call_user_func( $mainCallback );
+ return $this->release();
+ }
+}
+
+class PoolCounter_Stub extends PoolCounter {
+ public function acquire() {
+ return Status::newGood();
+ }
+
+ public function release() {
+ return Status::newGood();
+ }
+
+ public function wait() {
+ return Status::newGood();
+ }
+
+ public function executeProtected( $mainCallback, $dirtyCallback = false ) {
+ call_user_func( $mainCallback );
+ return Status::newGood();
+ }
+}
+
+
diff --git a/includes/Preferences.php b/includes/Preferences.php
new file mode 100644
index 00000000..70d88ec9
--- /dev/null
+++ b/includes/Preferences.php
@@ -0,0 +1,1389 @@
+<?php
+
+/**
+ * We're now using the HTMLForm object with some customisation to generate the
+ * Preferences form. This object handles generic submission, CSRF protection,
+ * layout and other logic in a reusable manner. We subclass it as a PreferencesForm
+ * to make some minor customisations.
+ *
+ * In order to generate the form, the HTMLForm object needs an array structure
+ * detailing the form fields available, and that's what this class is for. Each
+ * element of the array is a basic property-list, including the type of field,
+ * the label it is to be given in the form, callbacks for validation and
+ * 'filtering', and other pertinent information. Note that the 'default' field
+ * is named for generic forms, and does not represent the preference's default
+ * (which is stored in $wgDefaultUserOptions), but the default for the form
+ * field, which should be whatever the user has set for that preference. There
+ * is no need to override it unless you have some special storage logic (for
+ * instance, those not presently stored as options, but which are best set from
+ * the user preferences view).
+ *
+ * Field types are implemented as subclasses of the generic HTMLFormField
+ * object, and typically implement at least getInputHTML, which generates the
+ * HTML for the input field to be placed in the table.
+ *
+ * Once fields have been retrieved and validated, submission logic is handed
+ * over to the tryUISubmit static method of this class.
+ */
+class Preferences {
+ static $defaultPreferences = null;
+ static $saveFilters =
+ array(
+ 'timecorrection' => array( 'Preferences', 'filterTimezoneInput' ),
+ );
+
+ static function getPreferences( $user ) {
+ if ( self::$defaultPreferences )
+ return self::$defaultPreferences;
+
+ global $wgRCMaxAge;
+
+ $defaultPreferences = array();
+
+ self::profilePreferences( $user, $defaultPreferences );
+ self::skinPreferences( $user, $defaultPreferences );
+ self::filesPreferences( $user, $defaultPreferences );
+ self::mathPreferences( $user, $defaultPreferences );
+ self::datetimePreferences( $user, $defaultPreferences );
+ self::renderingPreferences( $user, $defaultPreferences );
+ self::editingPreferences( $user, $defaultPreferences );
+ self::rcPreferences( $user, $defaultPreferences );
+ self::watchlistPreferences( $user, $defaultPreferences );
+ self::searchPreferences( $user, $defaultPreferences );
+ self::miscPreferences( $user, $defaultPreferences );
+
+ wfRunHooks( 'GetPreferences', array( $user, &$defaultPreferences ) );
+
+ ## Remove preferences that wikis don't want to use
+ global $wgHiddenPrefs;
+ foreach ( $wgHiddenPrefs as $pref ) {
+ if ( isset( $defaultPreferences[$pref] ) ) {
+ unset( $defaultPreferences[$pref] );
+ }
+ }
+
+ ## Prod in defaults from the user
+ global $wgDefaultUserOptions;
+ foreach( $defaultPreferences as $name => &$info ) {
+ $prefFromUser = self::getOptionFromUser( $name, $info, $user );
+ $field = HTMLForm::loadInputFromParameters( $info ); // For validation
+ $defaultOptions = User::getDefaultOptions();
+ $globalDefault = isset( $defaultOptions[$name] )
+ ? $defaultOptions[$name]
+ : null;
+
+ // If it validates, set it as the default
+ if ( isset( $info['default'] ) ) {
+ // Already set, no problem
+ continue;
+ } elseif ( !is_null( $prefFromUser ) && // Make sure we're not just pulling nothing
+ $field->validate( $prefFromUser, $user->mOptions ) === true ) {
+ $info['default'] = $prefFromUser;
+ } elseif( $field->validate( $globalDefault, $user->mOptions ) === true ) {
+ $info['default'] = $globalDefault;
+ } else {
+ throw new MWException( "Global default '$globalDefault' is invalid for field $name" );
+ }
+ }
+
+ self::$defaultPreferences = $defaultPreferences;
+
+ return $defaultPreferences;
+ }
+
+ // Pull option from a user account. Handles stuff like array-type preferences.
+ static function getOptionFromUser( $name, $info, $user ) {
+ $val = $user->getOption( $name );
+
+ // Handling for array-type preferences
+ if ( ( isset( $info['type'] ) && $info['type'] == 'multiselect' ) ||
+ ( isset( $info['class'] ) && $info['class'] == 'HTMLMultiSelectField' ) ) {
+
+ $options = HTMLFormField::flattenOptions( $info['options'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $name;
+ $val = array();
+
+ foreach( $options as $label => $value ) {
+ if( $user->getOption( "$prefix$value" ) ) {
+ $val[] = $value;
+ }
+ }
+ }
+
+ return $val;
+ }
+
+ static function profilePreferences( $user, &$defaultPreferences ) {
+ global $wgLang, $wgUser;
+ ## User info #####################################
+ // Information panel
+ $defaultPreferences['username'] =
+ array(
+ 'type' => 'info',
+ 'label-message' => 'username',
+ 'default' => $user->getName(),
+ 'section' => 'personal/info',
+ );
+
+ $defaultPreferences['userid'] =
+ array(
+ 'type' => 'info',
+ 'label-message' => 'uid',
+ 'default' => $user->getId(),
+ 'section' => 'personal/info',
+ );
+
+ # Get groups to which the user belongs
+ $userEffectiveGroups = $user->getEffectiveGroups();
+ $userGroups = $userMembers = array();
+ foreach( $userEffectiveGroups as $ueg ) {
+ if( $ueg == '*' ) {
+ // Skip the default * group, seems useless here
+ continue;
+ }
+ $groupName = User::getGroupName( $ueg );
+ $userGroups[] = User::makeGroupLinkHTML( $ueg, $groupName );
+
+ $memberName = User::getGroupMember( $ueg );
+ $userMembers[] = User::makeGroupLinkHTML( $ueg, $memberName );
+ }
+ asort( $userGroups );
+ asort( $userMembers );
+
+ $defaultPreferences['usergroups'] =
+ array(
+ 'type' => 'info',
+ 'label' => wfMsgExt( 'prefs-memberingroups', 'parseinline',
+ $wgLang->formatNum( count($userGroups) ) ),
+ 'default' => wfMsgExt( 'prefs-memberingroups-type', array(),
+ $wgLang->commaList( $userGroups ),
+ $wgLang->commaList( $userMembers )
+ ),
+ 'raw' => true,
+ 'section' => 'personal/info',
+ );
+
+ $defaultPreferences['editcount'] =
+ array(
+ 'type' => 'info',
+ 'label-message' => 'prefs-edits',
+ 'default' => $wgLang->formatNum( $user->getEditCount() ),
+ 'section' => 'personal/info',
+ );
+
+ if( $user->getRegistration() ) {
+ $defaultPreferences['registrationdate'] =
+ array(
+ 'type' => 'info',
+ 'label-message' => 'prefs-registration',
+ 'default' => wfMsgExt( 'prefs-registration-date-time', 'parsemag',
+ $wgLang->timeanddate( $user->getRegistration(), true ),
+ $wgLang->date( $user->getRegistration(), true ),
+ $wgLang->time( $user->getRegistration(), true ) ),
+ 'section' => 'personal/info',
+ );
+ }
+
+ // Actually changeable stuff
+ global $wgAuth;
+ $defaultPreferences['realname'] =
+ array(
+ 'type' => $wgAuth->allowPropChange( 'realname' ) ? 'text' : 'info',
+ 'default' => $user->getRealName(),
+ 'section' => 'personal/info',
+ 'label-message' => 'yourrealname',
+ 'help-message' => 'prefs-help-realname',
+ );
+
+ $defaultPreferences['gender'] =
+ array(
+ 'type' => 'select',
+ 'section' => 'personal/info',
+ 'options' => array(
+ wfMsg( 'gender-male' ) => 'male',
+ wfMsg( 'gender-female' ) => 'female',
+ wfMsg( 'gender-unknown' ) => 'unknown',
+ ),
+ 'label-message' => 'yourgender',
+ 'help-message' => 'prefs-help-gender',
+ );
+
+ if( $wgAuth->allowPasswordChange() ) {
+ $link = $wgUser->getSkin()->link( SpecialPage::getTitleFor( 'Resetpass' ),
+ wfMsgHtml( 'prefs-resetpass' ), array(),
+ array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' ) ) );
+
+ $defaultPreferences['password'] =
+ array(
+ 'type' => 'info',
+ 'raw' => true,
+ 'default' => $link,
+ 'label-message' => 'yourpassword',
+ 'section' => 'personal/info',
+ );
+ }
+
+ $defaultPreferences['rememberpassword'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'tog-rememberpassword',
+ 'section' => 'personal/info',
+ );
+
+ // Language
+ global $wgContLanguageCode;
+ $languages = array_reverse( Language::getLanguageNames( false ) );
+ if( !array_key_exists( $wgContLanguageCode, $languages ) ) {
+ $languages[$wgContLanguageCode] = $wgContLanguageCode;
+ }
+ ksort( $languages );
+
+ $options = array();
+ foreach( $languages as $code => $name ) {
+ $display = wfBCP47( $code ) . ' - ' . $name;
+ $options[$display] = $code;
+ }
+ $defaultPreferences['language'] =
+ array(
+ 'type' => 'select',
+ 'section' => 'personal/i18n',
+ 'options' => $options,
+ 'label-message' => 'yourlanguage',
+ );
+
+ global $wgContLang, $wgDisableLangConversion;
+ global $wgDisableTitleConversion;
+ /* see if there are multiple language variants to choose from*/
+ $variantArray = array();
+ if( !$wgDisableLangConversion ) {
+ $variants = $wgContLang->getVariants();
+
+ $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 = array();
+ foreach( $variantArray as $code => $name ) {
+ $display = wfBCP47( $code ) . ' - ' . $name;
+ $options[$display] = $code;
+ }
+
+ if( count( $variantArray ) > 1 ) {
+ $defaultPreferences['variant'] =
+ array(
+ 'label-message' => 'yourvariant',
+ 'type' => 'select',
+ 'options' => $options,
+ 'section' => 'personal/i18n',
+ );
+ }
+ }
+
+ if( count( $variantArray ) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion ) {
+ $defaultPreferences['noconvertlink'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/i18n',
+ 'label-message' => 'tog-noconvertlink',
+ );
+ }
+
+ global $wgMaxSigChars, $wgOut, $wgParser;
+
+ // show a preview of the old signature first
+ $oldsigWikiText = $wgParser->preSaveTransform( "~~~", new Title , $user, new ParserOptions );
+ $oldsigHTML = $wgOut->parseInline( $oldsigWikiText );
+ $defaultPreferences['oldsig'] =
+ array(
+ 'type' => 'info',
+ 'raw' => true,
+ 'label-message' => 'tog-oldsig',
+ 'default' => $oldsigHTML,
+ 'section' => 'personal/signature',
+ );
+ $defaultPreferences['nickname'] =
+ array(
+ 'type' => $wgAuth->allowPropChange( 'nickname' ) ? 'text' : 'info',
+ 'maxlength' => $wgMaxSigChars,
+ 'label-message' => 'yournick',
+ 'validation-callback' =>
+ array( 'Preferences', 'validateSignature' ),
+ 'section' => 'personal/signature',
+ 'filter-callback' => array( 'Preferences', 'cleanSignature' ),
+ );
+ $defaultPreferences['fancysig'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'tog-fancysig',
+ 'help-message' => 'prefs-help-signature', // show general help about signature at the bottom of the section
+ 'section' => 'personal/signature'
+ );
+
+ ## Email stuff
+
+ global $wgEnableEmail;
+ if ($wgEnableEmail) {
+
+ global $wgEmailConfirmToEdit;
+
+ $defaultPreferences['emailaddress'] =
+ array(
+ 'type' => $wgAuth->allowPropChange( 'emailaddress' ) ? 'email' : 'info',
+ 'default' => $user->getEmail(),
+ 'section' => 'personal/email',
+ 'label-message' => 'youremail',
+ 'help-message' => $wgEmailConfirmToEdit
+ ? 'prefs-help-email-required'
+ : 'prefs-help-email',
+ 'validation-callback' => array( 'Preferences', 'validateEmail' ),
+ );
+
+ global $wgEnableUserEmail, $wgEmailAuthentication;
+
+ $disableEmailPrefs = false;
+
+ if ( $wgEmailAuthentication ) {
+ if ( $user->getEmail() ) {
+ if( $user->getEmailAuthenticationTimestamp() ) {
+ // date and time are separate parameters to facilitate localisation.
+ // $time is kept for backward compat reasons.
+ // 'emailauthenticated' is also used in SpecialConfirmemail.php
+ $time = $wgLang->timeAndDate( $user->getEmailAuthenticationTimestamp(), true );
+ $d = $wgLang->date( $user->getEmailAuthenticationTimestamp(), true );
+ $t = $wgLang->time( $user->getEmailAuthenticationTimestamp(), true );
+ $emailauthenticated = wfMsgExt( 'emailauthenticated', 'parseinline',
+ array($time, $d, $t ) ) . '<br />';
+ $disableEmailPrefs = false;
+ } else {
+ $disableEmailPrefs = true;
+ $skin = $wgUser->getSkin();
+ $emailauthenticated = wfMsgExt( 'emailnotauthenticated', 'parseinline' ) . '<br />' .
+ $skin->link(
+ SpecialPage::getTitleFor( 'Confirmemail' ),
+ wfMsg( 'emailconfirmlink' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ ) . '<br />';
+ }
+ } else {
+ $disableEmailPrefs = true;
+ $emailauthenticated = wfMsgHtml( 'noemailprefs' );
+ }
+
+ $defaultPreferences['emailauthentication'] =
+ array(
+ 'type' => 'info',
+ 'raw' => true,
+ 'section' => 'personal/email',
+ 'label-message' => 'prefs-emailconfirm-label',
+ 'default' => $emailauthenticated,
+ );
+
+ }
+
+ if( $wgEnableUserEmail && $user->isAllowed( 'sendemail' ) ) {
+ $defaultPreferences['disablemail'] =
+ array(
+ 'type' => 'toggle',
+ 'invert' => true,
+ 'section' => 'personal/email',
+ 'label-message' => 'allowemail',
+ 'disabled' => $disableEmailPrefs,
+ );
+ $defaultPreferences['ccmeonemails'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-ccmeonemails',
+ 'disabled' => $disableEmailPrefs,
+ );
+ }
+
+ global $wgEnotifWatchlist;
+ if ( $wgEnotifWatchlist ) {
+ $defaultPreferences['enotifwatchlistpages'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifwatchlistpages',
+ 'disabled' => $disableEmailPrefs,
+ );
+ }
+ global $wgEnotifUserTalk;
+ if( $wgEnotifUserTalk ) {
+ $defaultPreferences['enotifusertalkpages'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifusertalkpages',
+ 'disabled' => $disableEmailPrefs,
+ );
+ }
+ if( $wgEnotifUserTalk || $wgEnotifWatchlist ) {
+ $defaultPreferences['enotifminoredits'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifminoredits',
+ 'disabled' => $disableEmailPrefs,
+ );
+ }
+ $defaultPreferences['enotifrevealaddr'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'tog-enotifrevealaddr',
+ 'disabled' => $disableEmailPrefs,
+ );
+ }
+ }
+
+ static function skinPreferences( $user, &$defaultPreferences ) {
+ ## Skin #####################################
+ $defaultPreferences['skin'] =
+ array(
+ 'type' => 'radio',
+ 'options' => self::generateSkinOptions( $user ),
+ 'label' => '&nbsp;',
+ 'section' => 'rendering/skin',
+ );
+
+ $selectedSkin = $user->getOption( 'skin' );
+ if ( in_array( $selectedSkin, array( 'cologneblue', 'standard' ) ) ) {
+ global $wgLang;
+ $settings = array_flip( $wgLang->getQuickbarSettings() );
+
+ $defaultPreferences['quickbar'] =
+ array(
+ 'type' => 'radio',
+ 'options' => $settings,
+ 'section' => 'rendering/skin',
+ 'label-message' => 'qbsettings',
+ );
+ }
+ }
+
+ static function mathPreferences( $user, &$defaultPreferences ) {
+ ## Math #####################################
+ global $wgUseTeX, $wgLang;
+ if( $wgUseTeX ) {
+ $defaultPreferences['math'] =
+ array(
+ 'type' => 'radio',
+ 'options' =>
+ array_flip( array_map( 'wfMsgHtml', $wgLang->getMathNames() ) ),
+ 'label' => '&nbsp;',
+ 'section' => 'rendering/math',
+ );
+ }
+ }
+
+ static function filesPreferences( $user, &$defaultPreferences ) {
+ ## Files #####################################
+ $defaultPreferences['imagesize'] =
+ array(
+ 'type' => 'select',
+ 'options' => self::getImageSizes(),
+ 'label-message' => 'imagemaxsize',
+ 'section' => 'rendering/files',
+ );
+ $defaultPreferences['thumbsize'] =
+ array(
+ 'type' => 'select',
+ 'options' => self::getThumbSizes(),
+ 'label-message' => 'thumbsize',
+ 'section' => 'rendering/files',
+ );
+ }
+
+ static function datetimePreferences( $user, &$defaultPreferences ) {
+ global $wgLang;
+
+ ## Date and time #####################################
+ $dateOptions = self::getDateOptions();
+ if( $dateOptions ) {
+ $defaultPreferences['date'] =
+ array(
+ 'type' => 'radio',
+ 'options' => $dateOptions,
+ 'label' => '&nbsp;',
+ 'section' => 'datetime/dateformat',
+ );
+ }
+
+ // Info
+ $nowlocal = Xml::element( 'span', array( 'id' => 'wpLocalTime' ),
+ $wgLang->time( $now = wfTimestampNow(), true ) );
+ $nowserver = $wgLang->time( $now, false ) .
+ Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) );
+
+ $defaultPreferences['nowserver'] =
+ array(
+ 'type' => 'info',
+ 'raw' => 1,
+ 'label-message' => 'servertime',
+ 'default' => $nowserver,
+ 'section' => 'datetime/timeoffset',
+ );
+
+ $defaultPreferences['nowlocal'] =
+ array(
+ 'type' => 'info',
+ 'raw' => 1,
+ 'label-message' => 'localtime',
+ 'default' => $nowlocal,
+ 'section' => 'datetime/timeoffset',
+ );
+
+ // Grab existing pref.
+ $tzOffset = $user->getOption( 'timecorrection' );
+ $tz = explode( '|', $tzOffset, 2 );
+
+ $tzSetting = $tzOffset;
+ if( count( $tz ) > 1 && $tz[0] == 'Offset' ) {
+ $minDiff = $tz[1];
+ $tzSetting = sprintf( '%+03d:%02d', floor( $minDiff/60 ), abs( $minDiff )%60 );
+ }
+
+ $defaultPreferences['timecorrection'] =
+ array(
+ 'class' => 'HTMLSelectOrOtherField',
+ 'label-message' => 'timezonelegend',
+ 'options' => self::getTimezoneOptions(),
+ 'default' => $tzSetting,
+ 'size' => 20,
+ 'section' => 'datetime/timeoffset',
+ );
+ }
+
+ static function renderingPreferences( $user, &$defaultPreferences ) {
+ ## Page Rendering ##############################
+ $defaultPreferences['underline'] =
+ array(
+ 'type' => 'select',
+ 'options' => array(
+ wfMsg( 'underline-never' ) => 0,
+ wfMsg( 'underline-always' ) => 1,
+ wfMsg( 'underline-default' ) => 2,
+ ),
+ 'label-message' => 'tog-underline',
+ 'section' => 'rendering/advancedrendering',
+ );
+
+ $stubThresholdValues = array( 0, 50, 100, 500, 1000, 2000, 5000, 10000 );
+ $stubThresholdOptions = array();
+ foreach( $stubThresholdValues as $value ) {
+ $stubThresholdOptions[wfMsg( 'size-bytes', $value )] = $value;
+ }
+
+ $defaultPreferences['stubthreshold'] =
+ array(
+ 'type' => 'selectorother',
+ 'section' => 'rendering/advancedrendering',
+ 'options' => $stubThresholdOptions,
+ 'size' => 20,
+ 'label' => wfMsg( 'stub-threshold' ), // Raw HTML message. Yay?
+ );
+ $defaultPreferences['highlightbroken'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label' => wfMsg( 'tog-highlightbroken' ), // Raw HTML
+ );
+ $defaultPreferences['showtoc'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-showtoc',
+ );
+ $defaultPreferences['nocache'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'tog-nocache',
+ 'section' => 'rendering/advancedrendering',
+ );
+ $defaultPreferences['showhiddencats'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-showhiddencats'
+ );
+ $defaultPreferences['showjumplinks'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-showjumplinks',
+ );
+ $defaultPreferences['justify'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-justify',
+ );
+ $defaultPreferences['numberheadings'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rendering/advancedrendering',
+ 'label-message' => 'tog-numberheadings',
+ );
+ }
+
+ static function editingPreferences( $user, &$defaultPreferences ) {
+ global $wgUseExternalEditor, $wgLivePreview;
+
+ ## Editing #####################################
+ $defaultPreferences['cols'] =
+ array(
+ 'type' => 'int',
+ 'label-message' => 'columns',
+ 'section' => 'editing/textboxsize',
+ 'min' => 4,
+ 'max' => 1000,
+ );
+ $defaultPreferences['rows'] =
+ array(
+ 'type' => 'int',
+ 'label-message' => 'rows',
+ 'section' => 'editing/textboxsize',
+ 'min' => 4,
+ 'max' => 1000,
+ );
+
+ $defaultPreferences['editfont'] =
+ array(
+ 'type' => 'select',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'editfont-style',
+ 'options' => array(
+ wfMsg( 'editfont-default' ) => 'default',
+ wfMsg( 'editfont-monospace' ) => 'monospace',
+ wfMsg( 'editfont-sansserif' ) => 'sans-serif',
+ wfMsg( 'editfont-serif' ) => 'serif',
+ )
+ );
+ $defaultPreferences['previewontop'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-previewontop',
+ );
+ $defaultPreferences['previewonfirst'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-previewonfirst',
+ );
+ $defaultPreferences['editsection'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editsection',
+ );
+ $defaultPreferences['editsectiononrightclick'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editsectiononrightclick',
+ );
+ $defaultPreferences['editondblclick'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editondblclick',
+ );
+ $defaultPreferences['editwidth'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-editwidth',
+ );
+ $defaultPreferences['showtoolbar'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-showtoolbar',
+ );
+ $defaultPreferences['minordefault'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-minordefault',
+ );
+
+ if ( $wgUseExternalEditor ) {
+ $defaultPreferences['externaleditor'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-externaleditor',
+ );
+ $defaultPreferences['externaldiff'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-externaldiff',
+ );
+ }
+
+ $defaultPreferences['forceeditsummary'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-forceeditsummary',
+ );
+ if ( $wgLivePreview ) {
+ $defaultPreferences['uselivepreview'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'editing/advancedediting',
+ 'label-message' => 'tog-uselivepreview',
+ );
+ }
+ }
+
+ static function rcPreferences( $user, &$defaultPreferences ) {
+ global $wgRCMaxAge, $wgUseRCPatrol, $wgLang;
+ ## RecentChanges #####################################
+ $defaultPreferences['rcdays'] =
+ array(
+ 'type' => 'float',
+ 'label-message' => 'recentchangesdays',
+ 'section' => 'rc/display',
+ 'min' => 1,
+ 'max' => ceil( $wgRCMaxAge / ( 3600*24 ) ),
+ 'help' => wfMsgExt( 'recentchangesdays-max', array( 'parsemag' ), $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600*24 ) ) ) ),
+ );
+ $defaultPreferences['rclimit'] =
+ array(
+ 'type' => 'int',
+ 'label-message' => 'recentchangescount',
+ 'help-message' => 'prefs-help-recentchangescount',
+ 'section' => 'rc/display',
+ );
+ $defaultPreferences['usenewrc'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'tog-usenewrc',
+ 'section' => 'rc/advancedrc',
+ );
+ $defaultPreferences['hideminor'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'tog-hideminor',
+ 'section' => 'rc/advancedrc',
+ );
+
+ if( $wgUseRCPatrol ) {
+ $defaultPreferences['hidepatrolled'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rc/advancedrc',
+ 'label-message' => 'tog-hidepatrolled',
+ );
+ $defaultPreferences['newpageshidepatrolled'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rc/advancedrc',
+ 'label-message' => 'tog-newpageshidepatrolled',
+ );
+ }
+
+ global $wgRCShowWatchingUsers;
+ if( $wgRCShowWatchingUsers ) {
+ $defaultPreferences['shownumberswatching'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'rc/advancedrc',
+ 'label-message' => 'tog-shownumberswatching',
+ );
+ }
+ }
+
+ static function watchlistPreferences( $user, &$defaultPreferences ) {
+ global $wgUseRCPatrol, $wgEnableAPI;
+ ## Watchlist #####################################
+ $defaultPreferences['watchlistdays'] =
+ array(
+ 'type' => 'float',
+ 'min' => 0,
+ 'max' => 7,
+ 'section' => 'watchlist/display',
+ 'help' => wfMsgHtml( 'prefs-watchlist-days-max' ),
+ 'label-message' => 'prefs-watchlist-days',
+ );
+ $defaultPreferences['wllimit'] =
+ array(
+ 'type' => 'int',
+ 'min' => 0,
+ 'max' => 1000,
+ 'label-message' => 'prefs-watchlist-edits',
+ 'help' => wfMsgHtml( 'prefs-watchlist-edits-max' ),
+ 'section' => 'watchlist/display',
+ );
+ $defaultPreferences['extendwatchlist'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-extendwatchlist',
+ );
+ $defaultPreferences['watchlisthideminor'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideminor',
+ );
+ $defaultPreferences['watchlisthidebots'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthidebots',
+ );
+ $defaultPreferences['watchlisthideown'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideown',
+ );
+ $defaultPreferences['watchlisthideanons'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideanons',
+ );
+ $defaultPreferences['watchlisthideliu'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthideliu',
+ );
+ if ( $wgEnableAPI ) {
+ # Some random gibberish as a proposed default
+ $hash = sha1( mt_rand() . microtime( true ) );
+ $defaultPreferences['watchlisttoken'] =
+ array(
+ 'type' => 'text',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'prefs-watchlist-token',
+ 'help' => wfMsgHtml( 'prefs-help-watchlist-token', $hash )
+ );
+ }
+
+ if ( $wgUseRCPatrol ) {
+ $defaultPreferences['watchlisthidepatrolled'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => 'tog-watchlisthidepatrolled',
+ );
+ }
+
+ $watchTypes = array(
+ 'edit' => 'watchdefault',
+ 'move' => 'watchmoves',
+ 'delete' => 'watchdeletion'
+ );
+
+ // Kinda hacky
+ if( $user->isAllowed( 'createpage' ) || $user->isAllowed( 'createtalk' ) ) {
+ $watchTypes['read'] = 'watchcreations';
+ }
+
+ foreach( $watchTypes as $action => $pref ) {
+ if ( $user->isAllowed( $action ) ) {
+ $defaultPreferences[$pref] = array(
+ 'type' => 'toggle',
+ 'section' => 'watchlist/advancedwatchlist',
+ 'label-message' => "tog-$pref",
+ );
+ }
+ }
+ }
+
+ static function searchPreferences( $user, &$defaultPreferences ) {
+ global $wgContLang;
+
+ ## Search #####################################
+ $defaultPreferences['searchlimit'] =
+ array(
+ 'type' => 'int',
+ 'label-message' => 'resultsperpage',
+ 'section' => 'searchoptions/display',
+ 'min' => 0,
+ );
+ $defaultPreferences['contextlines'] =
+ array(
+ 'type' => 'int',
+ 'label-message' => 'contextlines',
+ 'section' => 'searchoptions/display',
+ 'min' => 0,
+ );
+ $defaultPreferences['contextchars'] =
+ array(
+ 'type' => 'int',
+ 'label-message' => 'contextchars',
+ 'section' => 'searchoptions/display',
+ 'min' => 0,
+ );
+ global $wgEnableMWSuggest;
+ if( $wgEnableMWSuggest ) {
+ $defaultPreferences['disablesuggest'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'mwsuggest-disable',
+ 'section' => 'searchoptions/display',
+ );
+ }
+
+ $defaultPreferences['searcheverything'] =
+ array(
+ 'type' => 'toggle',
+ 'label-message' => 'searcheverything-enable',
+ 'section' => 'searchoptions/advancedsearchoptions',
+ );
+
+ // Searchable namespaces back-compat with old format
+ $searchableNamespaces = SearchEngine::searchableNamespaces();
+
+ $nsOptions = array();
+ foreach( $wgContLang->getNamespaces() as $ns => $name ) {
+ if( $ns < 0 ) continue;
+ $displayNs = str_replace( '_', ' ', $name );
+
+ if( !$displayNs ) $displayNs = wfMsg( 'blanknamespace' );
+
+ $displayNs = htmlspecialchars( $displayNs );
+ $nsOptions[$displayNs] = $ns;
+ }
+
+ $defaultPreferences['searchnamespaces'] =
+ array(
+ 'type' => 'multiselect',
+ 'label-message' => 'defaultns',
+ 'options' => $nsOptions,
+ 'section' => 'searchoptions/advancedsearchoptions',
+ 'prefix' => 'searchNs',
+ );
+ }
+
+ static function miscPreferences( $user, &$defaultPreferences ) {
+ ## Misc #####################################
+ $defaultPreferences['diffonly'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'misc/diffs',
+ 'label-message' => 'tog-diffonly',
+ );
+ $defaultPreferences['norollbackdiff'] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'misc/diffs',
+ 'label-message' => 'tog-norollbackdiff',
+ );
+
+ // Stuff from Language::getExtraUserToggles()
+ global $wgContLang;
+
+ $toggles = $wgContLang->getExtraUserToggles();
+
+ foreach( $toggles as $toggle ) {
+ $defaultPreferences[$toggle] =
+ array(
+ 'type' => 'toggle',
+ 'section' => 'personal/i18n',
+ 'label-message' => "tog-$toggle",
+ );
+ }
+ }
+
+ /**
+ * @param object $user The user object
+ * @return array Text/links to display as key; $skinkey as value
+ */
+ static function generateSkinOptions( $user ) {
+ global $wgDefaultSkin, $wgLang, $wgAllowUserCss, $wgAllowUserJs;
+ $ret = array();
+
+ $mptitle = Title::newMainPage();
+ $previewtext = wfMsgHtml( 'skin-preview' );
+ # Only show members of Skin::getSkinNames() rather than
+ # $skinNames (skins is all skin names from Language.php)
+ $validSkinNames = Skin::getUsableSkins();
+ # Sort by UI skin name. First though need to update validSkinNames as sometimes
+ # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
+ foreach ( $validSkinNames as $skinkey => &$skinname ) {
+ $msgName = "skinname-{$skinkey}";
+ $localisedSkinName = wfMsg( $msgName );
+ if ( !wfEmptyMsg( $msgName, $localisedSkinName ) ) {
+ $skinname = htmlspecialchars( $localisedSkinName );
+ }
+ }
+ asort( $validSkinNames );
+ $sk = $user->getSkin();
+
+ foreach( $validSkinNames as $skinkey => $sn ) {
+ $linkTools = array();
+
+ # Mark the default skin
+ if( $skinkey == $wgDefaultSkin ) {
+ $linkTools[] = wfMsgHtml( 'default' );
+ }
+
+ # Create preview link
+ $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) );
+ $linkTools[] = "<a target='_blank' href=\"$mplink\">$previewtext</a>";
+
+ # Create links to user CSS/JS pages
+ if( $wgAllowUserCss ) {
+ $cssPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.css' );
+ $linkTools[] = $sk->link( $cssPage, wfMsgHtml( 'prefs-custom-css' ) );
+ }
+ if( $wgAllowUserJs ) {
+ $jsPage = Title::makeTitleSafe( NS_USER, $user->getName() . '/' . $skinkey . '.js' );
+ $linkTools[] = $sk->link( $jsPage, wfMsgHtml( 'prefs-custom-js' ) );
+ }
+
+ $display = $sn . ' ' . wfMsg( 'parentheses', $wgLang->pipeList( $linkTools ) );
+ $ret[$display] = $skinkey;
+ }
+
+ return $ret;
+ }
+
+ static function getDateOptions() {
+ global $wgLang;
+ $dateopts = $wgLang->getDatePreferences();
+
+ $ret = array();
+
+ if( $dateopts ) {
+ if ( !in_array( 'default', $dateopts ) ) {
+ $dateopts[] = 'default'; // Make sure default is always valid
+ // Bug 19237
+ }
+
+ $idCnt = 0;
+ $epoch = wfTimestampNow();
+ foreach( $dateopts as $key ) {
+ if( $key == 'default' ) {
+ $formatted = wfMsgHtml( 'datedefault' );
+ } else {
+ $formatted = htmlspecialchars( $wgLang->timeanddate( $epoch, false, $key ) );
+ }
+ $ret[$formatted] = $key;
+ }
+ }
+ return $ret;
+ }
+
+ static function getImageSizes() {
+ global $wgImageLimits;
+
+ $ret = array();
+
+ foreach ( $wgImageLimits as $index => $limits ) {
+ $display = "{$limits[0]}×{$limits[1]}" . wfMsg( 'unit-pixel' );
+ $ret[$display] = $index;
+ }
+
+ return $ret;
+ }
+
+ static function getThumbSizes() {
+ global $wgThumbLimits;
+
+ $ret = array();
+
+ foreach ( $wgThumbLimits as $index => $size ) {
+ $display = $size . wfMsg( 'unit-pixel' );
+ $ret[$display] = $index;
+ }
+
+ return $ret;
+ }
+
+ static function validateSignature( $signature, $alldata ) {
+ global $wgParser, $wgMaxSigChars, $wgLang;
+ if( mb_strlen( $signature ) > $wgMaxSigChars ) {
+ return
+ Xml::element( 'span', array( 'class' => 'error' ),
+ wfMsgExt( 'badsiglength', 'parsemag',
+ $wgLang->formatNum( $wgMaxSigChars )
+ )
+ );
+ } elseif( !empty( $alldata['fancysig'] ) &&
+ false === $wgParser->validateSig( $signature ) ) {
+ return Xml::element( 'span', array( 'class' => 'error' ), wfMsg( 'badsig' ) );
+ } else {
+ return true;
+ }
+ }
+
+ static function cleanSignature( $signature, $alldata ) {
+ global $wgParser;
+ if( $alldata['fancysig'] ) {
+ $signature = $wgParser->cleanSig( $signature );
+ } else {
+ // When no fancy sig used, make sure ~{3,5} get removed.
+ $signature = $wgParser->cleanSigInSig( $signature );
+ }
+
+ return $signature;
+ }
+
+ static function validateEmail( $email, $alldata ) {
+ if ( $email && !User::isValidEmailAddr( $email ) ) {
+ return wfMsgExt( 'invalidemailaddress', 'parseinline' );
+ }
+
+ global $wgEmailConfirmToEdit;
+ if( $wgEmailConfirmToEdit && !$email ) {
+ return wfMsgExt( 'noemailtitle', 'parseinline' );
+ }
+ return true;
+ }
+
+ static function getFormObject( $user ) {
+ $formDescriptor = Preferences::getPreferences( $user );
+ $htmlForm = new PreferencesForm( $formDescriptor, 'prefs' );
+
+ $htmlForm->setSubmitText( wfMsg( 'saveprefs' ) );
+ $htmlForm->setTitle( SpecialPage::getTitleFor( 'Preferences' ) );
+ $htmlForm->setSubmitID( 'prefsubmit' );
+ $htmlForm->setSubmitCallback( array( 'Preferences', 'tryFormSubmit' ) );
+
+ return $htmlForm;
+ }
+
+ static function getTimezoneOptions() {
+ $opt = array();
+
+ global $wgLocalTZoffset;
+
+ $opt[wfMsg( 'timezoneuseserverdefault' )] = "System|$wgLocalTZoffset";
+ $opt[wfMsg( 'timezoneuseoffset' )] = 'other';
+ $opt[wfMsg( 'guesstimezone' )] = 'guess';
+
+ if ( function_exists( 'timezone_identifiers_list' ) ) {
+ # Read timezone list
+ $tzs = timezone_identifiers_list();
+ sort( $tzs );
+
+ $tzRegions = array();
+ $tzRegions['Africa'] = wfMsg( 'timezoneregion-africa' );
+ $tzRegions['America'] = wfMsg( 'timezoneregion-america' );
+ $tzRegions['Antarctica'] = wfMsg( 'timezoneregion-antarctica' );
+ $tzRegions['Arctic'] = wfMsg( 'timezoneregion-arctic' );
+ $tzRegions['Asia'] = wfMsg( 'timezoneregion-asia' );
+ $tzRegions['Atlantic'] = wfMsg( 'timezoneregion-atlantic' );
+ $tzRegions['Australia'] = wfMsg( 'timezoneregion-australia' );
+ $tzRegions['Europe'] = wfMsg( 'timezoneregion-europe' );
+ $tzRegions['Indian'] = wfMsg( 'timezoneregion-indian' );
+ $tzRegions['Pacific'] = wfMsg( 'timezoneregion-pacific' );
+ asort( $tzRegions );
+
+ $prefill = array_fill_keys( array_values( $tzRegions ), array() );
+ $opt = array_merge( $opt, $prefill );
+
+ $now = date_create( 'now' );
+
+ foreach ( $tzs as $tz ) {
+ $z = explode( '/', $tz, 2 );
+
+ # timezone_identifiers_list() returns a number of
+ # backwards-compatibility entries. This filters them out of the
+ # list presented to the user.
+ if ( count( $z ) != 2 || !array_key_exists( $z[0], $tzRegions ) )
+ continue;
+
+ # Localize region
+ $z[0] = $tzRegions[$z[0]];
+
+ $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 );
+
+ $display = str_replace( '_', ' ', $z[0] . '/' . $z[1] );
+ $value = "ZoneInfo|$minDiff|$tz";
+
+ $opt[$z[0]][$display] = $value;
+ }
+ }
+ return $opt;
+ }
+
+ static function filterTimezoneInput( $tz, $alldata ) {
+ $data = explode( '|', $tz, 3 );
+ switch ( $data[0] ) {
+ case 'ZoneInfo':
+ case 'System':
+ return $tz;
+ default:
+ $data = explode( ':', $tz, 2 );
+ $minDiff = 0;
+ if( count( $data ) == 2 ) {
+ $data[0] = intval( $data[0] );
+ $data[1] = intval( $data[1] );
+ $minDiff = abs( $data[0] ) * 60 + $data[1];
+ if ( $data[0] < 0 ) $minDiff = -$minDiff;
+ } else {
+ $minDiff = intval( $data[0] ) * 60;
+ }
+
+ # Max is +14:00 and min is -12:00, see:
+ # http://en.wikipedia.org/wiki/Timezone
+ $minDiff = min( $minDiff, 840 ); # 14:00
+ $minDiff = max( $minDiff, -720 ); # -12:00
+ return 'Offset|'.$minDiff;
+ }
+ }
+
+ static function tryFormSubmit( $formData, $entryPoint = 'internal' ) {
+ global $wgUser, $wgEmailAuthentication, $wgEnableEmail;
+
+ $result = true;
+
+ // Filter input
+ foreach( array_keys( $formData ) as $name ) {
+ if ( isset( self::$saveFilters[$name] ) ) {
+ $formData[$name] =
+ call_user_func( self::$saveFilters[$name], $formData[$name], $formData );
+ }
+ }
+
+ // Stuff that shouldn't be saved as a preference.
+ $saveBlacklist = array(
+ 'realname',
+ 'emailaddress',
+ );
+
+ if( $wgEnableEmail ) {
+ $newadr = $formData['emailaddress'];
+ $oldadr = $wgUser->getEmail();
+ if( ( $newadr != '' ) && ( $newadr != $oldadr ) ) {
+ # the user has supplied a new email address on the login page
+ # 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 ) ) {
+ return wfMsg( 'mailerror', htmlspecialchars( $result->getMessage() ) );
+ } elseif( $entryPoint == 'ui' ) {
+ $result = 'eauth';
+ }
+ }
+ } else {
+ $wgUser->setEmail( $newadr );
+ }
+ if( $oldadr != $newadr ) {
+ wfRunHooks( 'PrefsEmailAudit', array( $wgUser, $oldadr, $newadr ) );
+ }
+ }
+
+ // Fortunately, the realname field is MUCH simpler
+ global $wgHiddenPrefs;
+ if ( !in_array( 'realname', $wgHiddenPrefs ) ) {
+ $realName = $formData['realname'];
+ $wgUser->setRealName( $realName );
+ }
+
+ foreach( $saveBlacklist as $b )
+ unset( $formData[$b] );
+
+ // Keeps old preferences from interfering due to back-compat
+ // code, etc.
+ $wgUser->resetOptions();
+
+ foreach( $formData as $key => $value ) {
+ $wgUser->setOption( $key, $value );
+ }
+
+ $wgUser->saveSettings();
+
+ return $result;
+ }
+
+ public static function tryUISubmit( $formData ) {
+ $res = self::tryFormSubmit( $formData, 'ui' );
+
+ if( $res ) {
+ $urlOptions = array( 'success' );
+ if( $res === 'eauth' )
+ $urlOptions[] = 'eauth';
+
+ $queryString = implode( '&', $urlOptions );
+
+ $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( $queryString );
+ global $wgOut;
+ $wgOut->redirect( $url );
+ }
+
+ return true;
+ }
+
+ public static function loadOldSearchNs( $user ) {
+ $searchableNamespaces = SearchEngine::searchableNamespaces();
+ // Back compat with old format
+ $arr = array();
+
+ foreach( $searchableNamespaces as $ns => $name ) {
+ if( $user->getOption( 'searchNs' . $ns ) ) {
+ $arr[] = $ns;
+ }
+ }
+
+ return $arr;
+ }
+}
+
+/** Some tweaks to allow js prefs to work */
+class PreferencesForm extends HTMLForm {
+
+ function wrapForm( $html ) {
+ $html = Xml::tags( 'div', array( 'id' => 'preferences' ), $html );
+
+ return parent::wrapForm( $html );
+ }
+
+ function getButtons() {
+ $html = parent::getButtons();
+
+ global $wgUser;
+
+ $sk = $wgUser->getSkin();
+ $t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
+
+ $html .= "\n" . $sk->link( $t, wfMsgHtml( 'restoreprefs' ) );
+
+ $html = Xml::tags( 'div', array( 'class' => 'mw-prefs-buttons' ), $html );
+
+ return $html;
+ }
+
+ function filterDataForSubmit( $data ) {
+ // Support for separating MultiSelect preferences into multiple preferences
+ // Due to lack of array support.
+ foreach( $this->mFlatFields as $fieldname => $field ) {
+ $info = $field->mParams;
+ if( $field instanceof HTMLMultiSelectField ) {
+ $options = HTMLFormField::flattenOptions( $info['options'] );
+ $prefix = isset( $info['prefix'] ) ? $info['prefix'] : $fieldname;
+
+ foreach( $options as $opt ) {
+ $data["$prefix$opt"] = in_array( $opt, $data[$fieldname] );
+ }
+
+ unset( $data[$fieldname] );
+ }
+ }
+
+ return $data;
+ }
+}
diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php
index 10c85930..930b29d4 100644
--- a/includes/PrefixSearch.php
+++ b/includes/PrefixSearch.php
@@ -3,17 +3,18 @@
/**
* PrefixSearch - Handles searching prefixes of titles and finding any page
* names that match. Used largely by the OpenSearch implementation.
- *
+ *
* @ingroup Search
*/
class PrefixSearch {
/**
* Do a prefix search of titles and return a list of matching page names.
- * @param string $search
- * @param int $limit
- * @param array $namespaces - used if query is not explicitely prefixed
- * @return array of strings
+ *
+ * @param $search String
+ * @param $limit Integer
+ * @param $namespaces Array: used if query is not explicitely prefixed
+ * @return Array of strings
*/
public static function titleSearch( $search, $limit, $namespaces=array() ) {
$search = trim( $search );
@@ -21,11 +22,11 @@ class PrefixSearch {
return array(); // Return empty result
}
$namespaces = self::validateNamespaces( $namespaces );
-
+
$title = Title::newFromText( $search );
if( $title && $title->getInterwiki() == '' ) {
$ns = array($title->getNamespace());
- if($ns[0] == NS_MAIN)
+ if($ns[0] == NS_MAIN)
$ns = $namespaces; // no explicit prefix, use default namespaces
return self::searchBackend(
$ns, $title->getText(), $limit );
@@ -39,17 +40,17 @@ class PrefixSearch {
return self::searchBackend(
array($title->getNamespace()), '', $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
+ * @param $namespaces Array
+ * @param $search String
+ * @param $limit Integer
+ * @return Array of strings
*/
protected static function searchBackend( $namespaces, $search, $limit ) {
if( count($namespaces) == 1 ){
@@ -69,6 +70,10 @@ class PrefixSearch {
/**
* Prefix search special-case for Special: namespace.
+ *
+ * @param $search String: term
+ * @param $limit Integer: max number of items to return
+ * @return Array
*/
protected static function specialSearch( $search, $limit ) {
global $wgContLang;
@@ -83,6 +88,9 @@ class PrefixSearch {
$keys[$wgContLang->caseFold( $page )] = $page;
}
foreach( $wgContLang->getSpecialPageAliases() as $page => $aliases ) {
+ if( !array_key_exists( $page, SpecialPage::$mList ) ) # bug 20885
+ continue;
+
foreach( $aliases as $alias ) {
$keys[$wgContLang->caseFold( $alias )] = $alias;
}
@@ -107,16 +115,16 @@ class PrefixSearch {
* be automatically capitalized by Title::secureAndSpit()
* later on depending on $wgCapitalLinks)
*
- * @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
+ * @param $namespaces Array: namespaces to search in
+ * @param $search String: term
+ * @param $limit Integer: max number of items to return
+ * @return Array of title strings
*/
protected static function defaultSearchBackend( $namespaces, $search, $limit ) {
$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
-
+ $ns = NS_MAIN; // if searching on many always default to main
+
// Prepare nested request
$req = new FauxRequest(array (
'action' => 'query',
@@ -143,11 +151,12 @@ class PrefixSearch {
return $srchres;
}
-
+
/**
* Validate an array of numerical namespace indexes
- *
- * @param array $namespaces
+ *
+ * @param $namespaces Array
+ * @return Array
*/
protected static function validateNamespaces($namespaces){
global $wgContLang;
@@ -161,7 +170,7 @@ class PrefixSearch {
if( count($valid) > 0 )
return $valid;
}
-
+
return array( NS_MAIN );
}
}
diff --git a/includes/Profiler.php b/includes/Profiler.php
index 80a6a68a..817b71ab 100644
--- a/includes/Profiler.php
+++ b/includes/Profiler.php
@@ -12,7 +12,7 @@ $wgProfiling = true;
/**
* Begin profiling of a function
- * @param $functioname name of the function we will profile
+ * @param $functionname name of the function we will profile
*/
function wfProfileIn( $functionname ) {
global $wgProfiler;
@@ -21,7 +21,7 @@ function wfProfileIn( $functionname ) {
/**
* Stop profiling of a function
- * @param $functioname name of the function we have profiled
+ * @param $functionname name of the function we have profiled
*/
function wfProfileOut( $functionname = 'missing' ) {
global $wgProfiler;
@@ -31,8 +31,8 @@ function wfProfileOut( $functionname = 'missing' ) {
/**
* Returns a profiling output to be stored in debug file
*
- * @param float $start
- * @param float $elapsed time elapsed since the beginning of the request
+ * @param $start Float
+ * @param $elapsed Float: time elapsed since the beginning of the request
*/
function wfGetProfilingOutput( $start, $elapsed ) {
global $wgProfiler;
@@ -128,6 +128,12 @@ class Profiler {
* called by wfProfileClose()
*/
function close() {
+ global $wgProfiling;
+
+ # Avoid infinite loop
+ if( !$wgProfiling )
+ return;
+
while( count( $this->mWorkStack ) ){
$this->profileOut( 'close' );
}
@@ -253,6 +259,7 @@ class Profiler {
wfProfileOut( '-overhead-total' );
# First, subtract the overhead!
+ $overheadTotal = $overheadMemory = $overheadInternal = array();
foreach( $this->mStack as $entry ){
$fname = $entry[0];
$start = $entry[2];
diff --git a/includes/ProfilerSimpleText.php b/includes/ProfilerSimpleText.php
index 9252e302..d3df3908 100644
--- a/includes/ProfilerSimpleText.php
+++ b/includes/ProfilerSimpleText.php
@@ -21,6 +21,10 @@ class ProfilerSimpleText extends ProfilerSimple {
public $visible=false; /* Show as <PRE> or <!-- ? */
function getFunctionReport() {
+ global $wgRequest;
+ if ( $wgRequest->getVal( 'action' ) == 'raw' ) # bug 20388
+ return;
+
if ($this->visible) print "<pre>";
else print "<!--\n";
uasort($this->mCollated,array('self','sort'));
diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php
index 5fe3cbc7..28df01ac 100644
--- a/includes/ProtectionForm.php
+++ b/includes/ProtectionForm.php
@@ -38,9 +38,9 @@ class ProtectionForm {
/** Map of action to "other" expiry time. Used in preference to mExpirySelection. */
var $mExpiry = array();
- /**
- * Map of action to value selected in expiry drop-down list.
- * Will be set to 'othertime' whenever mExpiry is set.
+ /**
+ * Map of action to value selected in expiry drop-down list.
+ * Will be set to 'othertime' whenever mExpiry is set.
*/
var $mExpirySelection = array();
@@ -54,20 +54,29 @@ class ProtectionForm {
var $mExistingExpiry = array();
function __construct( Article $article ) {
- global $wgRequest, $wgUser;
- global $wgRestrictionTypes, $wgRestrictionLevels;
+ global $wgUser;
+ // Set instance variables.
$this->mArticle = $article;
$this->mTitle = $article->mTitle;
- $this->mApplicableTypes = $this->mTitle->exists() ? $wgRestrictionTypes : array('create');
-
- $this->mCascade = $this->mTitle->areRestrictionsCascading();
-
- // The form will be available in read-only to show levels.
+ $this->mApplicableTypes = $this->mTitle->getRestrictionTypes();
+
+ // Check if the form should be disabled.
+ // If it is, the form will be available in read-only to show levels.
$this->mPermErrors = $this->mTitle->getUserPermissionsErrors('protect',$wgUser);
$this->disabled = wfReadOnly() || $this->mPermErrors != array();
$this->disabledAttrib = $this->disabled
? array( 'disabled' => 'disabled' )
: array();
+
+ $this->loadData();
+ }
+
+ // Loads the current state of protection into the object.
+ function loadData() {
+ global $wgRequest, $wgUser;
+ global $wgRestrictionLevels;
+
+ $this->mCascade = $this->mTitle->areRestrictionsCascading();
$this->mReason = $wgRequest->getText( 'mwProtect-reason' );
$this->mReasonSelection = $wgRequest->getText( 'wpProtectReasonSelection' );
@@ -76,6 +85,8 @@ class ProtectionForm {
foreach( $this->mApplicableTypes as $action ) {
// Fixme: this form currently requires individual selections,
// but the db allows multiples separated by commas.
+
+ // Pull the actual restriction from the DB
$this->mRestrictions[$action] = implode( '', $this->mTitle->getRestrictions( $action ) );
if ( !$this->mRestrictions[$action] ) {
@@ -127,7 +138,7 @@ class ProtectionForm {
}
}
- /**
+ /**
* Get the expiry time for a given action, by combining the relevant inputs.
* Returns a 14-char timestamp or "infinity", or false if the input was invalid
*/
@@ -180,7 +191,7 @@ class ProtectionForm {
list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
- if ( "" != $err ) {
+ if ( $err != "" ) {
$wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
$wgOut->addHTML( "<p class='error'>{$err}</p>\n" );
}
@@ -192,11 +203,11 @@ class ProtectionForm {
$titles .= '* [[:' . $title->getPrefixedText() . "]]\n";
}
- $wgOut->wrapWikiMsg( "$1\n$titles", array( 'protect-cascadeon', count($cascadeSources) ) );
+ $wgOut->wrapWikiMsg( "<div id=\"mw-protect-cascadeon\">\n$1\n" . $titles . "</div>", array( 'protect-cascadeon', count($cascadeSources) ) );
}
$sk = $wgUser->getSkin();
- $titleLink = $sk->makeLinkObj( $this->mTitle );
+ $titleLink = $sk->link( $this->mTitle );
$wgOut->setPageTitle( wfMsg( 'protect-title', $this->mTitle->getPrefixedText() ) );
$wgOut->setSubtitle( wfMsg( 'protect-backlink', $titleLink ) );
@@ -218,7 +229,7 @@ class ProtectionForm {
}
function save() {
- global $wgRequest, $wgUser, $wgOut;
+ global $wgRequest, $wgUser;
# Permission check!
if ( $this->disabled ) {
$this->show();
@@ -230,7 +241,7 @@ class ProtectionForm {
$this->show( wfMsg( 'sessionfailure' ) );
return false;
}
-
+
# Create reason string. Use list and/or custom string.
$reasonstr = $this->mReasonSelection;
if ( $reasonstr != 'other' && $this->mReason != '' ) {
@@ -258,7 +269,7 @@ class ProtectionForm {
# to a semi-protected page.
global $wgGroupPermissions;
- $edit_restriction = $this->mRestrictions['edit'];
+ $edit_restriction = isset( $this->mRestrictions['edit'] ) ? $this->mRestrictions['edit'] : '';
$this->mCascade = $wgRequest->getBool( 'mwProtect-cascade' );
if ($this->mCascade && ($edit_restriction != 'protect') &&
!(isset($wgGroupPermissions[$edit_restriction]['protect']) && $wgGroupPermissions[$edit_restriction]['protect'] ) )
@@ -274,7 +285,17 @@ class ProtectionForm {
throw new FatalError( "Unknown error at restriction save time." );
}
- if( $wgRequest->getCheck( 'mwProtectWatch' ) ) {
+ $errorMsg = '';
+ # Give extensions a change to handle added form items
+ if( !wfRunHooks( 'ProtectionForm::save', array($this->mArticle,&$errorMsg) ) ) {
+ throw new FatalError( "Unknown hook error at restriction save time." );
+ }
+ if( $errorMsg != '' ) {
+ $this->show( $errorMsg );
+ return false;
+ }
+
+ if( $wgRequest->getCheck( 'mwProtectWatch' ) && $wgUser->isLoggedIn() ) {
$this->mArticle->doWatch();
} elseif( $this->mTitle->userIsWatching() ) {
$this->mArticle->doUnwatch();
@@ -296,8 +317,8 @@ class ProtectionForm {
$out = '';
if( !$this->disabled ) {
$out .= $this->buildScript();
- $out .= Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
+ $out .= Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $this->mTitle->getLocalUrl( 'action=protect' ),
'id' => 'mw-Protect-Form', 'onsubmit' => 'ProtectionForm.enableUnchainedInputs(true)' ) );
$out .= Xml::hidden( 'wpEditToken',$wgUser->editToken() );
}
@@ -321,7 +342,7 @@ class ProtectionForm {
$reasonDropDown = Xml::listDropDown( 'wpProtectReasonSelection',
wfMsgForContent( 'protect-dropdown' ),
- wfMsgForContent( 'protect-otherreason-op' ),
+ wfMsgForContent( 'protect-otherreason-op' ),
$this->mReasonSelection,
'mwProtect-reason', 4 );
$scExpiryOptions = wfMsgForContent( 'protect-expiry-options' );
@@ -336,14 +357,14 @@ class ProtectionForm {
$timestamp = $wgLang->timeanddate( $this->mExistingExpiry[$action] );
$d = $wgLang->date( $this->mExistingExpiry[$action] );
$t = $wgLang->time( $this->mExistingExpiry[$action] );
- $expiryFormOptions .=
- Xml::option(
+ $expiryFormOptions .=
+ Xml::option(
wfMsg( 'protect-existing-expiry', $timestamp, $d, $t ),
'existing',
$this->mExpirySelection[$action] == 'existing'
) . "\n";
}
-
+
$expiryFormOptions .= Xml::option( wfMsg( 'protect-othertime-op' ), "othertime" ) . "\n";
foreach( explode(',', $scExpiryOptions) as $option ) {
if ( strpos($option, ":") === false ) {
@@ -375,7 +396,8 @@ class ProtectionForm {
}
# Add custom expiry field
$attribs = array( 'id' => "mwProtect-$action-expires",
- 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib;
+ 'onkeyup' => 'ProtectionForm.updateExpiry(this)',
+ 'onchange' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib;
$out .= "<table><tr>
<td class='mw-label'>" .
$mProtectother .
@@ -389,6 +411,8 @@ class ProtectionForm {
Xml::closeElement( 'fieldset' ) .
"</td></tr>";
}
+ # Give extensions a chance to add items to the form
+ wfRunHooks( 'ProtectionForm::buildForm', array($this->mArticle,&$out) );
$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
@@ -399,13 +423,13 @@ class ProtectionForm {
$out .= '<tr>
<td></td>
<td class="mw-input">' .
- Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade',
+ Xml::checkLabel( wfMsg( 'protect-cascade' ), 'mwProtect-cascade', 'mwProtect-cascade',
$this->mCascade, $this->disabledAttrib ) .
"</td>
</tr>\n";
$out .= Xml::closeElement( 'tbody' ) . Xml::closeElement( 'table' );
}
-
+
# Add manual and custom reason field/selects as well as submit
if( !$this->disabled ) {
$out .= Xml::openElement( 'table', array( 'id' => 'mw-protect-table3' ) ) .
@@ -424,10 +448,13 @@ class ProtectionForm {
{$mProtectreason}
</td>
<td class='mw-input'>" .
- Xml::input( 'mwProtect-reason', 60, $this->mReason, array( 'type' => 'text',
+ Xml::input( 'mwProtect-reason', 60, $this->mReason, array( 'type' => 'text',
'id' => 'mwProtect-reason', 'maxlength' => 255 ) ) .
"</td>
- </tr>
+ </tr>";
+ # Disallow watching is user is not logged in
+ if( $wgUser->isLoggedIn() ) {
+ $out .= "
<tr>
<td></td>
<td class='mw-input'>" .
@@ -435,7 +462,9 @@ class ProtectionForm {
'mwProtectWatch', 'mwProtectWatch',
$this->mTitle->userIsWatching() || $wgUser->getOption( 'watchdefault' ) ) .
"</td>
- </tr>
+ </tr>";
+ }
+ $out .= "
<tr>
<td></td>
<td class='mw-submit'>" .
@@ -447,8 +476,13 @@ class ProtectionForm {
$out .= Xml::closeElement( 'fieldset' );
if ( $wgUser->isAllowed( 'editinterface' ) ) {
- $linkTitle = Title::makeTitleSafe( NS_MEDIAWIKI, 'protect-dropdown' );
- $link = $wgUser->getSkin()->Link ( $linkTitle, wfMsgHtml( 'protect-edit-reasonlist' ) );
+ $title = Title::makeTitle( NS_MEDIAWIKI, 'Protect-dropdown' );
+ $link = $wgUser->getSkin()->link(
+ $title,
+ wfMsgHtml( 'protect-edit-reasonlist' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
$out .= '<p class="mw-protect-editreasons">' . $link . '</p>';
}
@@ -529,8 +563,8 @@ class ProtectionForm {
}
$script .= "[" . implode(',',$CascadeableLevels) . "];\n";
$options = (object)array(
- 'tableId' => 'mw-protect-table-move',
- 'labelText' => wfMsg( 'protect-unchain' ),
+ 'tableId' => 'mwProtectSet',
+ 'labelText' => wfMsg( 'protect-unchain-permissions' ),
'numTypes' => count($this->mApplicableTypes),
'existingMatch' => 1 == count( array_unique( $this->mExistingExpiry ) ),
);
@@ -548,5 +582,7 @@ class ProtectionForm {
# Show relevant lines from the protection log:
$out->addHTML( Xml::element( 'h2', null, LogPage::logName( 'protect' ) ) );
LogEventsList::showLogExtract( $out, 'protect', $this->mTitle->getPrefixedText() );
+ # Let extensions add other relevant log extracts
+ wfRunHooks( 'ProtectionForm::showLogExtract', array($this->mArticle,$out) );
}
}
diff --git a/includes/ProxyTools.php b/includes/ProxyTools.php
index 771fd577..5719e3e8 100644
--- a/includes/ProxyTools.php
+++ b/includes/ProxyTools.php
@@ -67,22 +67,26 @@ function wfGetAgent() {
* @return string
*/
function wfGetIP() {
- global $wgIP, $wgUsePrivateIPs;
+ global $wgIP, $wgUsePrivateIPs, $wgCommandLineMode;
# Return cached result
if ( !empty( $wgIP ) ) {
return $wgIP;
}
+ $ipchain = array();
+ $ip = false;
+
/* collect the originating ips */
# Client connecting to this webserver
if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
- $ipchain = array( IP::canonicalize( $_SERVER['REMOTE_ADDR'] ) );
- } else {
- # Running on CLI?
- $ipchain = array( '127.0.0.1' );
+ $ip = IP::canonicalize( $_SERVER['REMOTE_ADDR'] );
+ } elseif( $wgCommandLineMode ) {
+ $ip = '127.0.0.1';
+ }
+ if( $ip ) {
+ $ipchain[] = $ip;
}
- $ip = $ipchain[0];
# Append XFF on to $ipchain
$forwardedFor = wfGetForwardedFor();
@@ -107,6 +111,10 @@ function wfGetIP() {
}
}
+ if( !$ip ) {
+ throw new MWException( "Unable to determine IP" );
+ }
+
wfDebug( "IP: $ip\n" );
$wgIP = $ip;
return $ip;
@@ -116,7 +124,7 @@ function wfGetIP() {
* Checks if an IP is a trusted proxy providor
* Useful to tell if X-Fowarded-For data is possibly bogus
* Squid cache servers for the site and AOL are whitelisted
- * @param string $ip
+ * @param $ip String
* @return bool
*/
function wfIsTrustedProxy( $ip ) {
@@ -166,7 +174,7 @@ function wfProxyCheck() {
escapeshellarg( $port ),
escapeshellarg( $url )
));
- exec( "php $params &>/dev/null &" );
+ exec( "php $params >" . wfGetNull() . " 2>&1 &" );
}
# Set MemCached key
$wgMemc->set( $mcKey, 1, $wgProxyMemcExpiry );
@@ -187,12 +195,11 @@ function wfParseCIDR( $range ) {
*/
function wfIsLocallyBlockedProxy( $ip ) {
global $wgProxyList;
- $fname = 'wfIsLocallyBlockedProxy';
if ( !$wgProxyList ) {
return false;
}
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
if ( !is_array( $wgProxyList ) ) {
# Load from the specified file
@@ -209,7 +216,7 @@ function wfIsLocallyBlockedProxy( $ip ) {
} else {
$ret = false;
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $ret;
}
diff --git a/includes/QueryPage.php b/includes/QueryPage.php
index 1cef31ea..827264be 100644
--- a/includes/QueryPage.php
+++ b/includes/QueryPage.php
@@ -22,36 +22,36 @@ $wgQueryPages = array(
array( 'DisambiguationsPage', 'Disambiguations' ),
array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
array( 'LinkSearchPage', 'LinkSearch' ),
- array( 'ListredirectsPage', 'Listredirects' ),
+ array( 'ListredirectsPage', 'Listredirects' ),
array( 'LonelyPagesPage', 'Lonelypages' ),
array( 'LongPagesPage', 'Longpages' ),
array( 'MostcategoriesPage', 'Mostcategories' ),
array( 'MostimagesPage', 'Mostimages' ),
array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
- array( 'SpecialMostlinkedtemplates', 'Mostlinkedtemplates' ),
+ array( 'SpecialMostlinkedtemplates', 'Mostlinkedtemplates' ),
array( 'MostlinkedPage', 'Mostlinked' ),
array( 'MostrevisionsPage', 'Mostrevisions' ),
array( 'FewestrevisionsPage', 'Fewestrevisions' ),
array( 'ShortPagesPage', 'Shortpages' ),
array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
- array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
- array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
+ array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
+ array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
array( 'UnusedCategoriesPage', 'Unusedcategories' ),
array( 'UnusedimagesPage', 'Unusedimages' ),
array( 'WantedCategoriesPage', 'Wantedcategories' ),
array( 'WantedFilesPage', 'Wantedfiles' ),
array( 'WantedPagesPage', 'Wantedpages' ),
- array( 'WantedTemplatesPage', 'Wantedtemplates' ),
+ array( 'WantedTemplatesPage', 'Wantedtemplates' ),
array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
- array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
- array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
+ array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
+ array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
);
wfRunHooks( 'wgQueryPages', array( &$wgQueryPages ) );
global $wgDisableCounters;
if ( !$wgDisableCounters )
- $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
+ $wgQueryPages[] = array( 'PopularPagesPage', 'Popularpages' );
/**
@@ -79,7 +79,7 @@ class QueryPage {
/**
* A mutator for $this->listoutput;
*
- * @param bool $bool
+ * @param $bool Boolean
*/
function setListoutput( $bool ) {
$this->listoutput = $bool;
@@ -89,6 +89,8 @@ class QueryPage {
* Subclasses return their name here. Make sure the name is also
* specified in SpecialPage.php and in Language.php as a language message
* param.
+ *
+ * @return String
*/
function getName() {
return '';
@@ -124,6 +126,8 @@ class QueryPage {
/**
* Override to sort by increasing values
+ *
+ * @return Boolean
*/
function sortDescending() {
return true;
@@ -138,8 +142,10 @@ class QueryPage {
* Is this query expensive (for some definition of expensive)? Then we
* don't let it run in miser mode. $wgDisableQueryPages causes all query
* pages to be declared expensive. Some query pages are always expensive.
+ *
+ * @return Boolean
*/
- function isExpensive( ) {
+ function isExpensive() {
global $wgDisableQueryPages;
return $wgDisableQueryPages;
}
@@ -148,7 +154,7 @@ class QueryPage {
* Whether or not the output of the page in question is retrived from
* the database cache.
*
- * @return bool
+ * @return Boolean
*/
function isCached() {
global $wgMiserMode;
@@ -158,6 +164,8 @@ class QueryPage {
/**
* Sometime we dont want to build rss / atom feeds.
+ *
+ * @return Boolean
*/
function isSyndicated() {
return true;
@@ -168,6 +176,9 @@ class QueryPage {
* skin; you can use it for making links. The result is a single row of
* result data. You should be able to grab SQL results off of it.
* If the function return "false", the line output will be skipped.
+ *
+ * @param $skin Skin object
+ * @param $result Object: database row
*/
function formatResult( $skin, $result ) {
return '';
@@ -175,8 +186,10 @@ class QueryPage {
/**
* The content returned by this function will be output before any result
+ *
+ * @return String
*/
- function getPageHeader( ) {
+ function getPageHeader() {
return '';
}
@@ -184,7 +197,8 @@ class QueryPage {
* If using extra form wheely-dealies, return a set of parameters here
* as an associative array. They will be encoded and added to the paging
* links (prev/next/lengths).
- * @return array
+ *
+ * @return Array
*/
function linkParameters() {
return array();
@@ -196,17 +210,20 @@ class QueryPage {
* Setting this to return true, will call one more time wfFormatResult to
* be sure that the very last result is formatted and shown.
*/
- function tryLastResult( ) {
+ function tryLastResult() {
return false;
}
/**
* Clear the cache and save new results
+ *
+ * @param $limit Integer: limit for SQL statement
+ * @param $ignoreErrors Boolean: whether to ignore database errors
*/
function recache( $limit, $ignoreErrors = true ) {
$fname = get_class( $this ) . '::recache';
$dbw = wfGetDB( DB_MASTER );
- $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
+ $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), __METHOD__, 'vslow' ) );
if ( !$dbw || !$dbr ) {
return false;
}
@@ -229,30 +246,23 @@ class QueryPage {
if ( $res ) {
$num = $dbr->numRows( $res );
# Fetch results
- $insertSql = "INSERT INTO $querycache (qc_type,qc_namespace,qc_title,qc_value) VALUES ";
- $first = true;
+ $vals = array();
while ( $res && $row = $dbr->fetchObject( $res ) ) {
- if ( $first ) {
- $first = false;
- } else {
- $insertSql .= ',';
- }
if ( isset( $row->value ) ) {
$value = intval( $row->value ); // @bug 14414
} else {
$value = 0;
}
-
- $insertSql .= '(' .
- $dbw->addQuotes( $row->type ) . ',' .
- $dbw->addQuotes( $row->namespace ) . ',' .
- $dbw->addQuotes( $row->title ) . ',' .
- $dbw->addQuotes( $value ) . ')';
+
+ $vals[] = array('qc_type' => $row->type,
+ 'qc_namespace' => $row->namespace,
+ 'qc_title' => $row->title,
+ 'qc_value' => $value);
}
# Save results into the querycache table on the master
- if ( !$first ) {
- if ( !$dbw->query( $insertSql, $fname ) ) {
+ if ( count( $vals ) ) {
+ if ( !$dbw->insert( 'querycache', $vals, __METHOD__ ) ) {
// Set result to false to indicate error
$dbr->freeResult( $res );
$res = false;
@@ -311,10 +321,12 @@ class QueryPage {
$tRow = $dbr->fetchObject( $tRes );
if( $tRow ) {
- $updated = $wgLang->timeAndDate( $tRow->qci_timestamp, true, true );
+ $updated = $wgLang->timeanddate( $tRow->qci_timestamp, true, true );
+ $updateddate = $wgLang->date( $tRow->qci_timestamp, true, true );
+ $updatedtime = $wgLang->time( $tRow->qci_timestamp, true, true );
$wgOut->addMeta( 'Data-Cache-Time', $tRow->qci_timestamp );
$wgOut->addInlineScript( "var dataCacheTime = '{$tRow->qci_timestamp}';" );
- $wgOut->addWikiMsg( 'perfcachedts', $updated );
+ $wgOut->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime );
} else {
$wgOut->addWikiMsg( 'perfcached' );
}
@@ -381,12 +393,12 @@ class QueryPage {
* Format and output report results using the given information plus
* OutputPage
*
- * @param OutputPage $out OutputPage to print to
- * @param Skin $skin User skin to use
- * @param Database $dbr Database (read) connection to use
- * @param int $res Result pointer
- * @param int $num Number of available result rows
- * @param int $offset Paging offset
+ * @param $out OutputPage to print to
+ * @param $skin Skin: user skin to use
+ * @param $dbr Database (read) connection to use
+ * @param $res Integer: result pointer
+ * @param $num Integer: number of available result rows
+ * @param $offset Integer: paging offset
*/
protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
global $wgContLang;
@@ -495,7 +507,7 @@ class QueryPage {
*/
function feedResult( $row ) {
if( !isset( $row->title ) ) {
- return NULL;
+ return null;
}
$title = Title::MakeTitle( intval( $row->namespace ), $row->title );
if( $title ) {
@@ -514,7 +526,7 @@ class QueryPage {
$this->feedItemAuthor( $row ),
$comments);
} else {
- return NULL;
+ return null;
}
}
@@ -542,3 +554,85 @@ class QueryPage {
return $title->getFullURL();
}
}
+
+/**
+ * Class definition for a wanted query page like
+ * WantedPages, WantedTemplates, etc
+ */
+abstract class WantedQueryPage extends QueryPage {
+
+ function isExpensive() {
+ return true;
+ }
+
+ function isSyndicated() {
+ return false;
+ }
+
+ /**
+ * 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->link( $title ) . '</s>'
+ : $skin->link(
+ $title,
+ null,
+ array(),
+ array(),
+ array( 'broken' )
+ );
+ } else {
+ $pageLink = $skin->link(
+ $title,
+ null,
+ array(),
+ array(),
+ array( 'broken' )
+ );
+ }
+ return wfSpecialList( $pageLink, $this->makeWlhLink( $title, $skin, $result ) );
+ } else {
+ $tsafe = htmlspecialchars( $result->title );
+ return wfMsgHtml( 'wantedpages-badtitle', $tsafe );
+ }
+ }
+
+ /**
+ * Make a "what links here" link for a given title
+ *
+ * @param $title Title to make the link for
+ * @param $skin Skin object to use
+ * @param $result Object: result row
+ * @return string
+ */
+ private function makeWlhLink( $title, $skin, $result ) {
+ global $wgLang;
+ $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
+ $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
+ $wgLang->formatNum( $result->value ) );
+ return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) );
+ }
+}
diff --git a/includes/RawPage.php b/includes/RawPage.php
index b422d49e..8e515af3 100644
--- a/includes/RawPage.php
+++ b/includes/RawPage.php
@@ -2,7 +2,7 @@
/**
* Copyright (C) 2004 Gabriel Wicke <wicke@wikidev.net>
* http://wikidev.net/
- * Based on PageHistory and SpecialExport
+ * Based on HistoryPage and SpecialExport
*
* License: GPL (http://www.gnu.org/copyleft/gpl.html)
*
@@ -109,34 +109,9 @@ class RawPage {
}
function view() {
- global $wgOut, $wgScript;
+ global $wgOut, $wgScript, $wgRequest;
- if( isset( $_SERVER['SCRIPT_URL'] ) ) {
- # Normally we use PHP_SELF to get the URL to the script
- # as it was called, minus the query string.
- #
- # Some sites use Apache rewrite rules to handle subdomains,
- # and have PHP set up in a weird way that causes PHP_SELF
- # to contain the rewritten URL instead of the one that the
- # outside world sees.
- #
- # If in this mode, use SCRIPT_URL instead, which mod_rewrite
- # provides containing the "before" URL.
- $url = $_SERVER['SCRIPT_URL'];
- } else {
- $url = $_SERVER['PHP_SELF'];
- }
-
- if( $url == '' ) {
- # This will make the next check fail with a confusing error
- # message, so we should mention it separately.
- wfHttpError( 500, 'Internal Server Error',
- "\$_SERVER['PHP_SELF'] is not set. Perhaps you're using CGI" .
- " and haven't set cgi.fix_pathinfo = 1 in php.ini?" );
- return;
- }
-
- if( strcmp( $wgScript, $url ) ) {
+ if( $wgRequest->isPathInfoBad() ) {
# Internet Explorer will ignore the Content-Type header if it
# thinks it sees a file extension it recognizes. Make sure that
# all raw requests are done through the script node, which will
@@ -150,6 +125,7 @@ class RawPage {
#
# Just return a 403 Forbidden and get it over with.
wfHttpError( 403, 'Forbidden',
+ 'Invalid file extension found in PATH_INFO. ' .
'Raw pages must be accessed through the primary script entry point.' );
return;
}
@@ -159,7 +135,8 @@ class RawPage {
$mode = $this->mPrivateCache ? 'private' : 'public';
header( 'Cache-Control: '.$mode.', s-maxage='.$this->mSmaxage.', max-age='.$this->mMaxage );
- if( HTMLFileCache::useFileCache() ) {
+ global $wgUseFileCache;
+ if( $wgUseFileCache and HTMLFileCache::useFileCache() ) {
$cache = new HTMLFileCache( $this->mTitle, 'raw' );
if( $cache->isFileCacheGood( /* Assume up to date */ ) ) {
$cache->loadFromFileCache();
diff --git a/includes/RecentChange.php b/includes/RecentChange.php
index 8e3f1107..51b608d8 100644
--- a/includes/RecentChange.php
+++ b/includes/RecentChange.php
@@ -3,46 +3,45 @@
/**
* Utility class for creating new RC entries
* mAttribs:
- * rc_id id of the row in the recentchanges table
- * rc_timestamp time the entry was made
- * rc_cur_time timestamp on the cur row
- * rc_namespace namespace #
- * rc_title non-prefixed db key
- * rc_type is new entry, used to determine whether updating is necessary
- * rc_minor is minor
- * rc_cur_id page_id of associated page entry
- * rc_user user id who made the entry
- * rc_user_text user name who made the entry
- * rc_comment edit summary
- * rc_this_oldid rev_id associated with this entry (or zero)
- * rc_last_oldid rev_id associated with the entry before this one (or zero)
- * rc_bot is bot, hidden
- * rc_ip IP address of the user in dotted quad notation
- * rc_new obsolete, use rc_type==RC_NEW
- * 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
+ * rc_id id of the row in the recentchanges table
+ * rc_timestamp time the entry was made
+ * rc_cur_time timestamp on the cur row
+ * rc_namespace namespace #
+ * rc_title non-prefixed db key
+ * rc_type is new entry, used to determine whether updating is necessary
+ * rc_minor is minor
+ * rc_cur_id page_id of associated page entry
+ * rc_user user id who made the entry
+ * rc_user_text user name who made the entry
+ * rc_comment edit summary
+ * rc_this_oldid rev_id associated with this entry (or zero)
+ * rc_last_oldid rev_id associated with the entry before this one (or zero)
+ * rc_bot is bot, hidden
+ * rc_ip IP address of the user in dotted quad notation
+ * rc_new obsolete, use rc_type==RC_NEW
+ * 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
- * lastTimestamp timestamp of previous entry, used in WHERE clause during update
- * lang the interwiki prefix, automatically set in save()
+ * prefixedDBkey prefixed db key, used by external app via msg queue
+ * lastTimestamp timestamp of previous entry, used in WHERE clause during update
+ * lang the interwiki prefix, automatically set in save()
* oldSize text size before the change
* newSize text size after the change
*
- * temporary: not stored in the database
+ * temporary: not stored in the database
* notificationtimestamp
* numberofWatchingusers
*
* @todo document functions and variables
*/
-class RecentChange
-{
+class RecentChange {
var $mAttribs = array(), $mExtra = array();
var $mTitle = false, $mMovedToTitle = false;
var $numberofWatchingusers = 0 ; # Dummy to prevent error message in SpecialRecentchangeslinked
@@ -77,15 +76,15 @@ class RecentChange
$dbr->freeResult( $res );
return self::newFromRow( $row );
} else {
- return NULL;
+ return null;
}
}
/**
* Find the first recent change matching some specific conditions
*
- * @param array $conds Array of conditions
- * @param mixed $fname Override the method name in profiling/logs
+ * @param $conds Array of conditions
+ * @param $fname Mixed: override the method name in profiling/logs
* @return RecentChange
*/
public static function newFromConds( $conds, $fname = false ) {
@@ -119,6 +118,8 @@ class RecentChange
public function &getTitle() {
if( $this->mTitle === false ) {
$this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
+ # Make sure the correct page ID is process cached
+ $this->mTitle->resetArticleID( $this->mAttribs['rc_cur_id'] );
}
return $this->mTitle;
}
@@ -154,11 +155,11 @@ class RecentChange
# Fixup database timestamps
$this->mAttribs['rc_timestamp'] = $dbw->timestamp($this->mAttribs['rc_timestamp']);
$this->mAttribs['rc_cur_time'] = $dbw->timestamp($this->mAttribs['rc_cur_time']);
- $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'rc_rc_id_seq' );
+ $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
if( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id']==0 ) {
- unset ( $this->mAttribs['rc_cur_id'] );
+ unset( $this->mAttribs['rc_cur_id'] );
}
# Insert new row
@@ -208,10 +209,10 @@ class RecentChange
/**
* Send some text to UDP
- * @param string $line
- * @param string $prefix
- * @param string $address
- * @return bool success
+ * @param $line String: text to send
+ * @param $prefix String
+ * @param $address String: address
+ * @return Boolean: success
*/
public static function sendToUDP( $line, $address = '', $prefix = '' ) {
global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort;
@@ -236,8 +237,8 @@ class RecentChange
/**
* Remove newlines, carriage returns and decode html entites
- * @param string $line
- * @return string
+ * @param $text String
+ * @return String
*/
public static function cleanupForIRC( $text ) {
return Sanitizer::decodeCharReferences( str_replace( array( "\n", "\r" ), array( "", "" ), $text ) );
@@ -246,8 +247,8 @@ class RecentChange
/**
* Mark a given change as patrolled
*
- * @param mixed $change RecentChange or corresponding rc_id
- * @param bool $auto for automatic patrol
+ * @param $change Mixed: RecentChange or corresponding rc_id
+ * @param $auto Boolean: for automatic patrol
* @return See doMarkPatrolled(), or null if $change is not an existing rc_id
*/
public static function markPatrolled( $change, $auto = false ) {
@@ -264,7 +265,7 @@ class RecentChange
* Mark this RecentChange as patrolled
*
* NOTE: Can also return 'rcpatroldisabled', 'hookaborted' and 'markedaspatrollederror-noautopatrol' as errors
- * @param bool $auto for automatic patrol
+ * @param $auto Boolean: for automatic patrol
* @return array of permissions errors, see Title::getUserPermissionsErrors()
*/
public function doMarkPatrolled( $auto = false ) {
@@ -303,7 +304,7 @@ class RecentChange
/**
* Mark this RecentChange patrolled, without error checking
- * @return int Number of affected rows
+ * @return Integer: number of affected rows
*/
public function reallyMarkPatrolled() {
$dbw = wfGetDB( DB_MASTER );
@@ -331,35 +332,35 @@ class RecentChange
$rc = new RecentChange;
$rc->mAttribs = array(
- 'rc_timestamp' => $timestamp,
- 'rc_cur_time' => $timestamp,
- 'rc_namespace' => $title->getNamespace(),
- 'rc_title' => $title->getDBkey(),
- 'rc_type' => RC_EDIT,
- 'rc_minor' => $minor ? 1 : 0,
- 'rc_cur_id' => $title->getArticleID(),
- 'rc_user' => $user->getId(),
- 'rc_user_text' => $user->getName(),
- 'rc_comment' => $comment,
- 'rc_this_oldid' => $newId,
- 'rc_last_oldid' => $oldId,
- 'rc_bot' => $bot ? 1 : 0,
- 'rc_moved_to_ns' => 0,
- 'rc_moved_to_title' => '',
- 'rc_ip' => $ip,
- 'rc_patrolled' => intval($patrol),
- 'rc_new' => 0, # obsolete
- 'rc_old_len' => $oldSize,
- 'rc_new_len' => $newSize,
- 'rc_deleted' => 0,
- 'rc_logid' => 0,
- 'rc_log_type' => null,
- 'rc_log_action' => '',
- 'rc_params' => ''
+ 'rc_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $title->getNamespace(),
+ 'rc_title' => $title->getDBkey(),
+ 'rc_type' => RC_EDIT,
+ 'rc_minor' => $minor ? 1 : 0,
+ 'rc_cur_id' => $title->getArticleID(),
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $comment,
+ 'rc_this_oldid' => $newId,
+ 'rc_last_oldid' => $oldId,
+ 'rc_bot' => $bot ? 1 : 0,
+ 'rc_moved_to_ns' => 0,
+ 'rc_moved_to_title' => '',
+ 'rc_ip' => $ip,
+ 'rc_patrolled' => intval($patrol),
+ 'rc_new' => 0, # obsolete
+ 'rc_old_len' => $oldSize,
+ 'rc_new_len' => $newSize,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0,
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => ''
);
$rc->mExtra = array(
- 'prefixedDBkey' => $title->getPrefixedDBkey(),
+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => $lastTimestamp,
'oldSize' => $oldSize,
'newSize' => $newSize,
@@ -400,18 +401,18 @@ class RecentChange
'rc_moved_to_title' => '',
'rc_ip' => $ip,
'rc_patrolled' => intval($patrol),
- 'rc_new' => 1, # obsolete
+ 'rc_new' => 1, # obsolete
'rc_old_len' => 0,
- 'rc_new_len' => $size,
- 'rc_deleted' => 0,
- 'rc_logid' => 0,
- 'rc_log_type' => null,
- 'rc_log_action' => '',
- 'rc_params' => ''
+ 'rc_new_len' => $size,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0,
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => ''
);
$rc->mExtra = array(
- 'prefixedDBkey' => $title->getPrefixedDBkey(),
+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => 0,
'oldSize' => 0,
'newSize' => $size
@@ -431,37 +432,37 @@ class RecentChange
$rc = new RecentChange;
$rc->mAttribs = array(
- 'rc_timestamp' => $timestamp,
- 'rc_cur_time' => $timestamp,
- 'rc_namespace' => $oldTitle->getNamespace(),
- 'rc_title' => $oldTitle->getDBkey(),
- 'rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE,
- 'rc_minor' => 0,
- 'rc_cur_id' => $oldTitle->getArticleID(),
- 'rc_user' => $user->getId(),
- 'rc_user_text' => $user->getName(),
- 'rc_comment' => $comment,
- 'rc_this_oldid' => 0,
- 'rc_last_oldid' => 0,
- 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
- 'rc_moved_to_ns' => $newTitle->getNamespace(),
- 'rc_moved_to_title' => $newTitle->getDBkey(),
- 'rc_ip' => $ip,
- 'rc_new' => 0, # obsolete
- '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_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $oldTitle->getNamespace(),
+ 'rc_title' => $oldTitle->getDBkey(),
+ 'rc_type' => $overRedir ? RC_MOVE_OVER_REDIRECT : RC_MOVE,
+ 'rc_minor' => 0,
+ 'rc_cur_id' => $oldTitle->getArticleID(),
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $comment,
+ 'rc_this_oldid' => 0,
+ 'rc_last_oldid' => 0,
+ 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot' , true ) : 0,
+ 'rc_moved_to_ns' => $newTitle->getNamespace(),
+ 'rc_moved_to_title' => $newTitle->getDBkey(),
+ 'rc_ip' => $ip,
+ 'rc_new' => 0, # obsolete
+ '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(
- 'prefixedDBkey' => $oldTitle->getPrefixedDBkey(),
+ 'prefixedDBkey' => $oldTitle->getPrefixedDBkey(),
'lastTimestamp' => 0,
- 'prefixedMoveTo' => $newTitle->getPrefixedDBkey()
+ 'prefixedMoveTo' => $newTitle->getPrefixedDBkey()
);
$rc->save();
}
@@ -499,34 +500,34 @@ class RecentChange
$rc = new RecentChange;
$rc->mAttribs = array(
- 'rc_timestamp' => $timestamp,
- 'rc_cur_time' => $timestamp,
- 'rc_namespace' => $target->getNamespace(),
- 'rc_title' => $target->getDBkey(),
- 'rc_type' => RC_LOG,
- 'rc_minor' => 0,
- 'rc_cur_id' => $target->getArticleID(),
- 'rc_user' => $user->getId(),
- 'rc_user_text' => $user->getName(),
- 'rc_comment' => $logComment,
- 'rc_this_oldid' => 0,
- 'rc_last_oldid' => 0,
- 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
- 'rc_moved_to_ns' => 0,
- 'rc_moved_to_title' => '',
- 'rc_ip' => $ip,
- 'rc_patrolled' => 1,
- '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_timestamp' => $timestamp,
+ 'rc_cur_time' => $timestamp,
+ 'rc_namespace' => $target->getNamespace(),
+ 'rc_title' => $target->getDBkey(),
+ 'rc_type' => RC_LOG,
+ 'rc_minor' => 0,
+ 'rc_cur_id' => $target->getArticleID(),
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_comment' => $logComment,
+ 'rc_this_oldid' => 0,
+ 'rc_last_oldid' => 0,
+ 'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
+ 'rc_moved_to_ns' => 0,
+ 'rc_moved_to_title' => '',
+ 'rc_ip' => $ip,
+ 'rc_patrolled' => 1,
+ '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(),
+ 'prefixedDBkey' => $title->getPrefixedDBkey(),
'lastTimestamp' => 0,
'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
);
@@ -579,7 +580,11 @@ class RecentChange
* @return mixed
*/
public function getAttribute( $name ) {
- return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : NULL;
+ return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
+ }
+
+ public function getAttributes() {
+ return $this->mAttribs;
}
/**
@@ -694,7 +699,7 @@ class RecentChange
if( $new === 0 ) {
$new = $this->mAttribs['rc_new_len'];
}
- if( $old === NULL || $new === NULL ) {
+ if( $old === null || $new === null ) {
return '';
}
return ChangesList::showCharacterDifference( $old, $new );
diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php
index 91cff40b..aba18362 100644
--- a/includes/RefreshLinksJob.php
+++ b/includes/RefreshLinksJob.php
@@ -111,8 +111,9 @@ class RefreshLinksJob2 extends Job {
$update = new LinksUpdate( $title, $parserOutput, false );
$update->doUpdate();
wfProfileOut( __METHOD__.'-update' );
- wfProfileOut( __METHOD__ );
+ wfWaitForSlaves( 5 );
}
+ wfProfileOut( __METHOD__ );
return true;
}
diff --git a/includes/Revision.php b/includes/Revision.php
index 8a2149c0..8d2c7e9d 100644
--- a/includes/Revision.php
+++ b/includes/Revision.php
@@ -1,8 +1,4 @@
<?php
-/**
- * @todo document
- * @file
- */
/**
* @todo document
@@ -12,7 +8,8 @@ class Revision {
const DELETED_COMMENT = 2;
const DELETED_USER = 4;
const DELETED_RESTRICTED = 8;
-
+ // Convenience field
+ const SUPPRESSED_USER = 12;
// Audience options for Revision::getText()
const FOR_PUBLIC = 1;
const FOR_THIS_USER = 2;
@@ -22,9 +19,8 @@ class Revision {
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
- * @param int $id
- * @access public
- * @static
+ * @param $id Integer
+ * @return Revision or null
*/
public static function newFromId( $id ) {
return Revision::newFromConds(
@@ -37,9 +33,9 @@ class Revision {
* that's attached to a given title. If not attached
* to that title, will return null.
*
- * @param Title $title
- * @param int $id
- * @return Revision
+ * @param $title Title
+ * @param $id Integer
+ * @return Revision or null
*/
public static function newFromTitle( $title, $id = 0 ) {
$conds = array(
@@ -67,13 +63,36 @@ class Revision {
}
/**
+ * Make a fake revision object from an archive table row. This is queried
+ * for permissions or even inserted (as in Special:Undelete)
+ * @todo Fixme: should be a subclass for RevisionDelete. [TS]
+ */
+ public static function newFromArchiveRow( $row, $overrides = array() ) {
+ $attribs = $overrides + array(
+ 'page' => isset( $row->page_id ) ? $row->page_id : null,
+ 'id' => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
+ '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' => isset( $row->ar_text_id ) ? $row->ar_text_id : null,
+ 'deleted' => $row->ar_deleted,
+ 'len' => $row->ar_len);
+ if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
+ // Pre-1.5 ar_text row
+ $attribs['text'] = self::getRevisionText( $row, 'ar_' );
+ }
+ return new self( $attribs );
+ }
+
+ /**
* Load a page revision from a given revision ID number.
* Returns null if no such revision can be found.
*
- * @param Database $db
- * @param int $id
- * @access public
- * @static
+ * @param $db DatabaseBase
+ * @param $id Integer
+ * @return Revision or null
*/
public static function loadFromId( $db, $id ) {
return Revision::loadFromConds( $db,
@@ -86,19 +105,17 @@ class Revision {
* that's attached to a given page. If not attached
* to that page, will return null.
*
- * @param Database $db
- * @param int $pageid
- * @param int $id
- * @return Revision
- * @access public
- * @static
+ * @param $db DatabaseBase
+ * @param $pageid Integer
+ * @param $id Integer
+ * @return Revision or null
*/
public static function loadFromPageId( $db, $pageid, $id = 0 ) {
- $conds=array('page_id=rev_page','rev_page'=>intval( $pageid ), 'page_id'=>intval( $pageid ));
+ $conds = array( 'page_id=rev_page','rev_page' => intval( $pageid ), 'page_id'=>intval( $pageid ) );
if( $id ) {
- $conds['rev_id']=intval($id);
+ $conds['rev_id'] = intval( $id );
} else {
- $conds[]='rev_id=page_latest';
+ $conds[] = 'rev_id=page_latest';
}
return Revision::loadFromConds( $db, $conds );
}
@@ -108,12 +125,10 @@ class Revision {
* that's attached to a given page. If not attached
* to that page, will return null.
*
- * @param Database $db
- * @param Title $title
- * @param int $id
- * @return Revision
- * @access public
- * @static
+ * @param $db DatabaseBase
+ * @param $title Title
+ * @param $id Integer
+ * @return Revision or null
*/
public static function loadFromTitle( $db, $title, $id = 0 ) {
if( $id ) {
@@ -134,12 +149,10 @@ class Revision {
* WARNING: Timestamps may in some circumstances not be unique,
* so this isn't the best key to use.
*
- * @param Database $db
- * @param Title $title
- * @param string $timestamp
- * @return Revision
- * @access public
- * @static
+ * @param $db Database
+ * @param $title Title
+ * @param $timestamp String
+ * @return Revision or null
*/
public static function loadFromTimestamp( $db, $title, $timestamp ) {
return Revision::loadFromConds(
@@ -153,12 +166,10 @@ class Revision {
/**
* Given a set of conditions, fetch a revision.
*
- * @param array $conditions
- * @return Revision
- * @access private
- * @static
+ * @param $conditions Array
+ * @return Revision or null
*/
- private static function newFromConds( $conditions ) {
+ public static function newFromConds( $conditions ) {
$db = wfGetDB( DB_SLAVE );
$row = Revision::loadFromConds( $db, $conditions );
if( is_null( $row ) && wfGetLB()->getServerCount() > 1 ) {
@@ -172,11 +183,9 @@ class Revision {
* Given a set of conditions, fetch a revision from
* the given database connection.
*
- * @param Database $db
- * @param array $conditions
- * @return Revision
- * @access private
- * @static
+ * @param $db Database
+ * @param $conditions Array
+ * @return Revision or null
*/
private static function loadFromConds( $db, $conditions ) {
$res = Revision::fetchFromConds( $db, $conditions );
@@ -197,28 +206,8 @@ class Revision {
* fetch all of a given page's revisions in turn.
* Each row can be fed to the constructor to get objects.
*
- * @param Title $title
+ * @param $title Title
* @return ResultWrapper
- * @access public
- * @static
- */
- public static function fetchAllRevisions( $title ) {
- return Revision::fetchFromConds(
- wfGetDB( DB_SLAVE ),
- array( 'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBkey(),
- 'page_id=rev_page' ) );
- }
-
- /**
- * Return a wrapper for a series of database rows to
- * fetch all of a given page's revisions in turn.
- * Each row can be fed to the constructor to get objects.
- *
- * @param Title $title
- * @return ResultWrapper
- * @access public
- * @static
*/
public static function fetchRevision( $title ) {
return Revision::fetchFromConds(
@@ -234,11 +223,9 @@ class Revision {
* which will return matching database rows with the
* fields necessary to build Revision objects.
*
- * @param Database $db
- * @param array $conditions
+ * @param $db Database
+ * @param $conditions Array
* @return ResultWrapper
- * @access private
- * @static
*/
private static function fetchFromConds( $db, $conditions ) {
$fields = self::selectFields();
@@ -285,6 +272,7 @@ class Revision {
'old_flags'
);
}
+
/**
* Return the list of page fields that should be selected from page table
*/
@@ -297,7 +285,9 @@ class Revision {
}
/**
- * @param object $row
+ * Constructor
+ *
+ * @param $row Mixed: either a database row or an array
* @access private
*/
function Revision( $row ) {
@@ -363,20 +353,17 @@ class Revision {
$this->mCurrent = false;
# If we still have no len_size, see it we have the text to figure it out
if ( !$this->mSize )
- $this->mSize = is_null($this->mText) ? null : strlen($this->mText);
+ $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
} else {
throw new MWException( 'Revision constructor passed invalid row format.' );
}
- $this->mUnpatrolled = NULL;
+ $this->mUnpatrolled = null;
}
- /**#@+
- * @access public
- */
-
/**
* Get revision ID
- * @return int
+ *
+ * @return Integer
*/
public function getId() {
return $this->mId;
@@ -384,7 +371,8 @@ class Revision {
/**
* Get text row ID
- * @return int
+ *
+ * @return Integer
*/
public function getTextId() {
return $this->mTextId;
@@ -392,7 +380,8 @@ class Revision {
/**
* Get parent revision ID (the original previous page revision)
- * @return int
+ *
+ * @return Integer
*/
public function getParentId() {
return $this->mParentId;
@@ -400,7 +389,8 @@ class Revision {
/**
* Returns the length of the text in this revision, or null if unknown.
- * @return int
+ *
+ * @return Integer
*/
public function getSize() {
return $this->mSize;
@@ -408,6 +398,7 @@ class Revision {
/**
* Returns the title of the page associated with this entry.
+ *
* @return Title
*/
public function getTitle() {
@@ -430,7 +421,8 @@ class Revision {
/**
* Set the title of the revision
- * @param Title $title
+ *
+ * @param $title Title
*/
public function setTitle( $title ) {
$this->mTitle = $title;
@@ -438,7 +430,8 @@ class Revision {
/**
* Get the page ID
- * @return int
+ *
+ * @return Integer
*/
public function getPage() {
return $this->mPage;
@@ -449,13 +442,13 @@ class Revision {
* If the specified audience does not have access to it, zero will be
* returned.
*
- * @param integer $audience One of:
+ * @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the ID regardless of permissions
*
*
- * @return int
+ * @return Integer
*/
public function getUser( $audience = self::FOR_PUBLIC ) {
if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
@@ -469,7 +462,8 @@ class Revision {
/**
* Fetch revision's user id without regard for the current user's permissions
- * @return string
+ *
+ * @return String
*/
public function getRawUser() {
return $this->mUser;
@@ -480,7 +474,7 @@ class Revision {
* If the specified audience does not have access to the username, an
* empty string will be returned.
*
- * @param integer $audience One of:
+ * @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
@@ -489,9 +483,9 @@ class Revision {
*/
public function getUserText( $audience = self::FOR_PUBLIC ) {
if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
- return "";
+ return '';
} elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER ) ) {
- return "";
+ return '';
} else {
return $this->mUserText;
}
@@ -499,7 +493,8 @@ class Revision {
/**
* Fetch revision's username without regard for view restrictions
- * @return string
+ *
+ * @return String
*/
public function getRawUserText() {
return $this->mUserText;
@@ -510,18 +505,18 @@ class Revision {
* If the specified audience does not have access to the comment, an
* empty string will be returned.
*
- * @param integer $audience One of:
+ * @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
*
- * @return string
+ * @return String
*/
function getComment( $audience = self::FOR_PUBLIC ) {
if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
- return "";
+ return '';
} elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT ) ) {
- return "";
+ return '';
} else {
return $this->mComment;
}
@@ -529,24 +524,25 @@ class Revision {
/**
* Fetch revision comment without regard for the current user's permissions
- * @return string
+ *
+ * @return String
*/
public function getRawComment() {
return $this->mComment;
}
/**
- * @return bool
+ * @return Boolean
*/
public function isMinor() {
return (bool)$this->mMinorEdit;
}
/**
- * @return int rcid of the unpatrolled row, zero if there isn't one
+ * @return Integer rcid of the unpatrolled row, zero if there isn't one
*/
public function isUnpatrolled() {
- if( $this->mUnpatrolled !== NULL ) {
+ if( $this->mUnpatrolled !== null ) {
return $this->mUnpatrolled;
}
$dbr = wfGetDB( DB_SLAVE );
@@ -565,15 +561,16 @@ class Revision {
/**
* int $field one of DELETED_* bitfield constants
- * @return bool
+ *
+ * @return Boolean
*/
public function isDeleted( $field ) {
- return ($this->mDeleted & $field) == $field;
+ return ( $this->mDeleted & $field ) == $field;
}
-
+
/**
* Get the deletion bitfield of the revision
- */
+ */
public function getVisibility() {
return (int)$this->mDeleted;
}
@@ -583,19 +580,19 @@ class Revision {
* If the specified audience does not have the ability to view this
* revision, an empty string will be returned.
*
- * @param integer $audience One of:
+ * @param $audience Integer: one of:
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
*
*
- * @return string
+ * @return String
*/
public function getText( $audience = self::FOR_PUBLIC ) {
if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
- return "";
+ return '';
} elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT ) ) {
- return "";
+ return '';
} else {
return $this->getRawText();
}
@@ -603,6 +600,8 @@ class Revision {
/**
* Alias for getText(Revision::FOR_THIS_USER)
+ *
+ * @return String
*/
public function revText() {
return $this->getText( self::FOR_THIS_USER );
@@ -610,7 +609,8 @@ class Revision {
/**
* Fetch revision text without regard for view restrictions
- * @return string
+ *
+ * @return String
*/
public function getRawText() {
if( is_null( $this->mText ) ) {
@@ -621,14 +621,14 @@ class Revision {
}
/**
- * @return string
+ * @return String
*/
public function getTimestamp() {
- return wfTimestamp(TS_MW, $this->mTimestamp);
+ return wfTimestamp( TS_MW, $this->mTimestamp );
}
/**
- * @return bool
+ * @return Boolean
*/
public function isCurrent() {
return $this->mCurrent;
@@ -636,7 +636,8 @@ class Revision {
/**
* Get previous revision for this title
- * @return Revision
+ *
+ * @return Revision or null
*/
public function getPrevious() {
if( $this->getTitle() ) {
@@ -649,7 +650,9 @@ class Revision {
}
/**
- * @return Revision
+ * Get next revision for this title
+ *
+ * @return Revision or null
*/
public function getNext() {
if( $this->getTitle() ) {
@@ -664,11 +667,12 @@ class Revision {
/**
* Get previous revision Id for this page_id
* This is used to populate rev_parent_id on save
- * @param Database $db
- * @return int
+ *
+ * @param $db DatabaseBase
+ * @return Integer
*/
private function getPreviousRevisionId( $db ) {
- if( is_null($this->mPage) ) {
+ if( is_null( $this->mPage ) ) {
return 0;
}
# Use page_latest if ID is not given
@@ -682,7 +686,7 @@ class Revision {
__METHOD__,
array( 'ORDER BY' => 'rev_id DESC' ) );
}
- return intval($prevId);
+ return intval( $prevId );
}
/**
@@ -690,9 +694,9 @@ class Revision {
* $row is usually an object from wfFetchRow(), both the flags and the text
* field must be included
*
- * @param object $row The text data
- * @param string $prefix table prefix (default 'old_')
- * @return string $text|false the text requested
+ * @param $row Object: the text data
+ * @param $prefix String: table prefix (default 'old_')
+ * @return String: text the text requested or false on failure
*/
public static function getRevisionText( $row, $prefix = 'old_' ) {
wfProfileIn( __METHOD__ );
@@ -716,13 +720,13 @@ class Revision {
# Use external methods for external objects, text in table is URL-only then
if ( in_array( 'external', $flags ) ) {
- $url=$text;
- @list(/* $proto */,$path)=explode('://',$url,2);
- if ($path=="") {
+ $url = $text;
+ @list(/* $proto */, $path ) = explode( '://', $url, 2 );
+ if( $path == '' ) {
wfProfileOut( __METHOD__ );
return false;
}
- $text=ExternalStore::fetchFromURL($url);
+ $text = ExternalStore::fetchFromURL( $url );
}
// If the text was fetched without an error, convert it
@@ -746,7 +750,9 @@ class Revision {
}
global $wgLegacyEncoding;
- if( $wgLegacyEncoding && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) ) {
+ if( $text !== false && $wgLegacyEncoding
+ && !in_array( 'utf-8', $flags ) && !in_array( 'utf8', $flags ) )
+ {
# Old revisions kept around in a legacy encoding?
# Upconvert on demand.
# ("utf8" checked for compatibility with some broken
@@ -766,8 +772,8 @@ class Revision {
* data is compressed, and 'utf-8' if we're saving in UTF-8
* mode.
*
- * @param mixed $text reference to a text
- * @return string
+ * @param $text Mixed: reference to a text
+ * @return String
*/
public static function compressRevisionText( &$text ) {
global $wgCompressRevisions;
@@ -792,8 +798,8 @@ class Revision {
* Insert a new revision into the database, returning the new revision ID
* number on success and dies horribly on failure.
*
- * @param Database $dbw
- * @return int
+ * @param $dbw DatabaseBase (master connection)
+ * @return Integer
*/
public function insertOn( $dbw ) {
global $wgDefaultExternalStore;
@@ -818,7 +824,7 @@ class Revision {
# Record the text (or external storage URL) to the text table
if( !isset( $this->mTextId ) ) {
- $old_id = $dbw->nextSequenceValue( 'text_old_id_val' );
+ $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
$dbw->insert( 'text',
array(
'old_id' => $old_id,
@@ -829,10 +835,12 @@ class Revision {
$this->mTextId = $dbw->insertId();
}
+ if ( $this->mComment === null ) $this->mComment = "";
+
# Record the edit in revisions
$rev_id = isset( $this->mId )
? $this->mId
- : $dbw->nextSequenceValue( 'rev_rev_id_val' );
+ : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
$dbw->insert( 'revision',
array(
'rev_id' => $rev_id,
@@ -844,16 +852,16 @@ class Revision {
'rev_user_text' => $this->mUserText,
'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ),
'rev_deleted' => $this->mDeleted,
- 'rev_len' => $this->mSize,
+ 'rev_len' => $this->mSize,
'rev_parent_id' => is_null($this->mParentId) ?
$this->getPreviousRevisionId( $dbw ) : $this->mParentId
), __METHOD__
);
- $this->mId = !is_null($rev_id) ? $rev_id : $dbw->insertId();
-
+ $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
+
wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
-
+
wfProfileOut( __METHOD__ );
return $this->mId;
}
@@ -862,17 +870,19 @@ class Revision {
* Lazy-load the revision's text.
* Currently hardcoded to the 'text' table storage engine.
*
- * @return string
+ * @return String
*/
- private function loadText() {
+ protected function loadText() {
wfProfileIn( __METHOD__ );
// Caching may be beneficial for massive use of external storage
global $wgRevisionCacheExpiry, $wgMemc;
- $key = wfMemcKey( 'revisiontext', 'textid', $this->getTextId() );
+ $textId = $this->getTextId();
+ $key = wfMemcKey( 'revisiontext', 'textid', $textId );
if( $wgRevisionCacheExpiry ) {
$text = $wgMemc->get( $key );
if( is_string( $text ) ) {
+ wfDebug( __METHOD__ . ": got id $textId from cache\n" );
wfProfileOut( __METHOD__ );
return $text;
}
@@ -924,11 +934,11 @@ class Revision {
* Such revisions can for instance identify page rename
* operations and other such meta-modifications.
*
- * @param Database $dbw
- * @param int $pageId ID number of the page to read from
- * @param string $summary
- * @param bool $minor
- * @return Revision
+ * @param $dbw DatabaseBase
+ * @param $pageId Integer: ID number of the page to read from
+ * @param $summary String: revision's summary
+ * @param $minor Boolean: whether the revision should be considered as minor
+ * @return Mixed: Revision, or null on error
*/
public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
wfProfileIn( __METHOD__ );
@@ -962,34 +972,56 @@ class Revision {
/**
* Determine if the current user is allowed to view a particular
* field of this revision, if it's marked as deleted.
- * @param int $field one of self::DELETED_TEXT,
- * self::DELETED_COMMENT,
- * self::DELETED_USER
- * @return bool
+ *
+ * @param $field Integer:one of self::DELETED_TEXT,
+ * self::DELETED_COMMENT,
+ * self::DELETED_USER
+ * @return Boolean
*/
public function userCan( $field ) {
- if( ( $this->mDeleted & $field ) == $field ) {
+ return self::userCanBitfield( $this->mDeleted, $field );
+ }
+
+ /**
+ * Determine if the current user is allowed to view a particular
+ * field of this revision, if it's marked as deleted. This is used
+ * by various classes to avoid duplication.
+ *
+ * @param $bitfield Integer: current field
+ * @param $field Integer: one of self::DELETED_TEXT = File::DELETED_FILE,
+ * self::DELETED_COMMENT = File::DELETED_COMMENT,
+ * self::DELETED_USER = File::DELETED_USER
+ * @return Boolean
+ */
+ public static function userCanBitfield( $bitfield, $field ) {
+ if( $bitfield & $field ) { // aspect is deleted
global $wgUser;
- $permission = ( $this->mDeleted & self::DELETED_RESTRICTED ) == self::DELETED_RESTRICTED
- ? 'suppressrevision'
- : 'deleterevision';
- wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+ $permission = '';
+ if ( $bitfield & self::DELETED_RESTRICTED ) {
+ $permission = 'suppressrevision';
+ } elseif ( $field & self::DELETED_TEXT ) {
+ $permission = 'deletedtext';
+ } else {
+ $permission = 'deletedhistory';
+ }
+ wfDebug( "Checking for $permission due to $field match on $bitfield\n" );
return $wgUser->isAllowed( $permission );
} else {
return true;
}
}
-
/**
* Get rev_timestamp from rev_id, without loading the rest of the row
- * @param Title $title
- * @param integer $id
+ *
+ * @param $title Title
+ * @param $id Integer
+ * @return String
*/
static function getTimestampFromId( $title, $id ) {
$dbr = wfGetDB( DB_SLAVE );
// Casting fix for DB2
- if ($id == '') {
+ if ( $id == '' ) {
$id = 0;
}
$conds = array( 'rev_id' => $id );
@@ -1005,8 +1037,10 @@ class Revision {
/**
* Get count of revisions per page...not very efficient
- * @param Database $db
- * @param int $id, page id
+ *
+ * @param $db DatabaseBase
+ * @param $id Integer: page id
+ * @return Integer
*/
static function countByPageId( $db, $id ) {
$row = $db->selectRow( 'revision', 'COUNT(*) AS revCount',
@@ -1019,8 +1053,10 @@ class Revision {
/**
* Get count of revisions per page...not very efficient
- * @param Database $db
- * @param Title $title
+ *
+ * @param $db DatabaseBase
+ * @param $title Title
+ * @return Integer
*/
static function countByTitle( $db, $title ) {
$id = $title->getArticleId();
diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php
index 678bfcfb..f6a9773d 100644
--- a/includes/Sanitizer.php
+++ b/includes/Sanitizer.php
@@ -43,7 +43,7 @@ define( 'MW_CHAR_REFS_REGEX',
$attrib = '[A-Za-z0-9]';
$space = '[\x09\x0a\x0d\x20]';
define( 'MW_ATTRIBS_REGEX',
- "/(?:^|$space)($attrib+)
+ "/(?:^|$space)((?:xml:|xmlns:)?$attrib+)
($space*=$space*
(?:
# The attribute value: quoted or alone
@@ -57,6 +57,16 @@ define( 'MW_ATTRIBS_REGEX',
)?(?=$space|\$)/sx" );
/**
+ * Regular expression to match URIs that could trigger script execution
+ */
+define( 'MW_EVIL_URI_PATTERN', '!(^|\s|\*/\s*)(javascript|vbscript)([^\w]|$)!i' );
+
+/**
+ * Regular expression to match namespace attributes
+ */
+define( 'MW_XMLNS_ATTRIBUTE_PATTRN', "/^xmlns:$attrib+$/" );
+
+/**
* List of all named character entities defined in HTML 4.01
* http://www.w3.org/TR/html4/sgml/entities.html
* @private
@@ -335,28 +345,30 @@ class Sanitizer {
* Cleans up HTML, removes dangerous tags and attributes, and
* removes HTML comments
* @private
- * @param string $text
- * @param callback $processCallback to do any variable or parameter replacements in HTML attribute values
- * @param array $args for the processing callback
+ * @param $text String
+ * @param $processCallback Callback to do any variable or parameter replacements in HTML attribute values
+ * @param $args Array for the processing callback
+ * @param $extratags Array for any extra tags to include
+ * @param $removetags Array for any tags (default or extra) to exclude
* @return string
*/
- static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array() ) {
+ static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array(), $removetags = array() ) {
global $wgUseTidy;
- static $htmlpairs, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
- $htmllist, $listtags, $htmlsingleallowed, $htmlelements, $staticInitialised;
+ static $htmlpairsStatic, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
+ $htmllist, $listtags, $htmlsingleallowed, $htmlelementsStatic, $staticInitialised;
wfProfileIn( __METHOD__ );
if ( !$staticInitialised ) {
- $htmlpairs = array_merge( $extratags, array( # Tags that must be closed
+ $htmlpairsStatic = array( # Tags that must be closed
'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
'strike', 'strong', 'tt', 'var', 'div', 'center',
'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
- 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u'
- ) );
+ 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'u', 'abbr'
+ );
$htmlsingle = array(
'br', 'hr', 'li', 'dt', 'dd'
);
@@ -377,53 +389,64 @@ class Sanitizer {
'li',
);
- $htmlsingleallowed = array_merge( $htmlsingle, $tabletags );
- $htmlelements = array_merge( $htmlsingle, $htmlpairs, $htmlnest );
+ $htmlsingleallowed = array_unique( array_merge( $htmlsingle, $tabletags ) );
+ $htmlelementsStatic = array_unique( array_merge( $htmlsingle, $htmlpairsStatic, $htmlnest ) );
# Convert them all to hashtables for faster lookup
- $vars = array( 'htmlpairs', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
- 'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelements' );
+ $vars = array( 'htmlpairsStatic', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
+ 'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelementsStatic' );
foreach ( $vars as $var ) {
$$var = array_flip( $$var );
}
$staticInitialised = true;
}
+ # Populate $htmlpairs and $htmlelements with the $extratags and $removetags arrays
+ $extratags = array_flip( $extratags );
+ $removetags = array_flip( $removetags );
+ $htmlpairs = array_merge( $extratags, $htmlpairsStatic );
+ $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ) , $removetags );
# Remove HTML comments
$text = Sanitizer::removeHTMLcomments( $text );
$bits = explode( '<', $text );
$text = str_replace( '>', '&gt;', array_shift( $bits ) );
- if(!$wgUseTidy) {
+ if ( !$wgUseTidy ) {
$tagstack = $tablestack = array();
foreach ( $bits as $x ) {
$regs = array();
+ # $slash: Does the current element start with a '/'?
+ # $t: Current element name
+ # $params: String between element name and >
+ # $brace: Ending '>' or '/>'
+ # $rest: Everything until the next element of $bits
if( preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) {
list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
} else {
$slash = $t = $params = $brace = $rest = null;
}
- $badtag = 0 ;
+ $badtag = false;
if ( isset( $htmlelements[$t = strtolower( $t )] ) ) {
# Check our stack
- if ( $slash ) {
- # Closing a tag...
- if( isset( $htmlsingleonly[$t] ) ) {
- $badtag = 1;
- } elseif ( ( $ot = @array_pop( $tagstack ) ) != $t ) {
+ if ( $slash && isset( $htmlsingleonly[$t] ) ) {
+ $badtag = true;
+ } elseif ( $slash ) {
+ # Closing a tag... is it the one we just opened?
+ $ot = @array_pop( $tagstack );
+ if ( $ot != $t ) {
if ( isset( $htmlsingleallowed[$ot] ) ) {
# Pop all elements with an optional close tag
# and see if we find a match below them
$optstack = array();
- array_push ($optstack, $ot);
- while ( ( ( $ot = @array_pop( $tagstack ) ) != $t ) &&
- isset( $htmlsingleallowed[$ot] ) )
- {
- array_push ($optstack, $ot);
+ array_push( $optstack, $ot );
+ $ot = @array_pop( $tagstack );
+ while ( $ot != $t && isset( $htmlsingleallowed[$ot] ) ) {
+ array_push( $optstack, $ot );
+ $ot = @array_pop( $tagstack );
}
if ( $t != $ot ) {
- # No match. Push the optinal elements back again
- $badtag = 1;
+ # No match. Push the optional elements back again
+ $badtag = true;
while ( $ot = @array_pop( $optstack ) ) {
array_push( $tagstack, $ot );
}
@@ -431,8 +454,8 @@ class Sanitizer {
} else {
@array_push( $tagstack, $ot );
# <li> can be nested in <ul> or <ol>, skip those cases:
- if(!(isset( $htmllist[$ot] ) && isset( $listtags[$t] ) )) {
- $badtag = 1;
+ if ( !isset( $htmllist[$ot] ) || !isset( $listtags[$t] ) ) {
+ $badtag = true;
}
}
} else {
@@ -444,23 +467,23 @@ class Sanitizer {
} else {
# Keep track for later
if ( isset( $tabletags[$t] ) &&
- ! in_array( 'table', $tagstack ) ) {
- $badtag = 1;
- } else if ( in_array( $t, $tagstack ) &&
- ! isset( $htmlnest [$t ] ) ) {
- $badtag = 1 ;
+ !in_array( 'table', $tagstack ) ) {
+ $badtag = true;
+ } elseif ( in_array( $t, $tagstack ) &&
+ !isset( $htmlnest [$t ] ) ) {
+ $badtag = true;
# Is it a self closed htmlpair ? (bug 5487)
- } else if( $brace == '/>' &&
+ } elseif ( $brace == '/>' &&
isset( $htmlpairs[$t] ) ) {
- $badtag = 1;
- } elseif( isset( $htmlsingleonly[$t] ) ) {
+ $badtag = true;
+ } elseif ( isset( $htmlsingleonly[$t] ) ) {
# Hack to force empty tag for uncloseable elements
$brace = '/>';
- } else if( isset( $htmlsingle[$t] ) ) {
+ } elseif ( isset( $htmlsingle[$t] ) ) {
# Hack to not close $htmlsingle tags
- $brace = NULL;
- } else if( isset( $tabletags[$t] )
- && in_array($t ,$tagstack) ) {
+ $brace = null;
+ } elseif ( isset( $tabletags[$t] )
+ && in_array( $t, $tagstack ) ) {
// New table tag but forgot to close the previous one
$text .= "</$t>";
} else {
@@ -480,7 +503,7 @@ class Sanitizer {
# Strip non-approved attributes from the tag
$newparams = Sanitizer::fixTagAttributes( $params, $t );
}
- if ( ! $badtag ) {
+ if ( !$badtag ) {
$rest = str_replace( '>', '&gt;', $rest );
$close = ( $brace == '/>' && !$slash ) ? ' /' : '';
$text .= "<$slash$t$newparams$close>$rest";
@@ -523,7 +546,7 @@ class Sanitizer {
* trailing spaces and one of the newlines.
*
* @private
- * @param string $text
+ * @param $text String
* @return string
*/
static function removeHTMLcomments( $text ) {
@@ -569,9 +592,9 @@ class Sanitizer {
* - Unsafe style attributes are discarded
* - Invalid id attributes are reencoded
*
- * @param array $attribs
- * @param string $element
- * @return array
+ * @param $attribs Array
+ * @param $element String
+ * @return Array
*
* @todo Check for legal values where the DTD limits things.
* @todo Check for unique id attribute :P
@@ -589,20 +612,34 @@ class Sanitizer {
* - Unsafe style attributes are discarded
* - Invalid id attributes are reencoded
*
- * @param array $attribs
- * @param array $whitelist list of allowed attribute names
- * @return array
+ * @param $attribs Array
+ * @param $whitelist Array: list of allowed attribute names
+ * @return Array
*
* @todo Check for legal values where the DTD limits things.
* @todo Check for unique id attribute :P
*/
static function validateAttributes( $attribs, $whitelist ) {
+ global $wgAllowRdfaAttributes, $wgAllowMicrodataAttributes;
+
$whitelist = array_flip( $whitelist );
+ $hrefExp = '/^(' . wfUrlProtocols() . ')[^\s]+$/';
+
$out = array();
foreach( $attribs as $attribute => $value ) {
+ #allow XML namespace declaration if RDFa is enabled
+ if ( $wgAllowRdfaAttributes && preg_match( MW_XMLNS_ATTRIBUTE_PATTRN, $attribute ) ) {
+ if ( !preg_match( MW_EVIL_URI_PATTERN, $value ) ) {
+ $out[$attribute] = $value;
+ }
+
+ continue;
+ }
+
if( !isset( $whitelist[$attribute] ) ) {
continue;
}
+
# Strip javascript "expression" from stylesheets.
# http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp
if( $attribute == 'style' ) {
@@ -610,15 +647,58 @@ class Sanitizer {
}
if ( $attribute === 'id' ) {
- global $wgEnforceHtmlIds;
- $value = Sanitizer::escapeId( $value,
- $wgEnforceHtmlIds ? 'noninitial' : 'xml' );
+ $value = Sanitizer::escapeId( $value, 'noninitial' );
+ }
+
+ //RDFa and microdata properties allow URLs, URIs and/or CURIs. check them for sanity
+ if ( $attribute === 'rel' || $attribute === 'rev' ||
+ $attribute === 'about' || $attribute === 'property' || $attribute === 'resource' || #RDFa
+ $attribute === 'datatype' || $attribute === 'typeof' || #RDFa
+ $attribute === 'itemid' || $attribute === 'itemprop' || $attribute === 'itemref' || #HTML5 microdata
+ $attribute === 'itemscope' || $attribute === 'itemtype' ) { #HTML5 microdata
+
+ //Paranoia. Allow "simple" values but suppress javascript
+ if ( preg_match( MW_EVIL_URI_PATTERN, $value ) ) {
+ continue;
+ }
+ }
+
+ # NOTE: even though elements using href/src are not allowed directly, supply
+ # validation code that can be used by tag hook handlers, etc
+ if ( $attribute === 'href' || $attribute === 'src' ) {
+ if ( !preg_match( $hrefExp, $value ) ) {
+ continue; //drop any href or src attributes not using an allowed protocol.
+ //NOTE: this also drops all relative URLs
+ }
}
// If this attribute was previously set, override it.
// Output should only have one attribute of each name.
$out[$attribute] = $value;
}
+
+ if ( $wgAllowMicrodataAttributes ) {
+ # There are some complicated validity constraints we need to
+ # enforce here. First of all, we don't want to allow non-standard
+ # itemtypes.
+ $allowedTypes = array(
+ 'http://microformats.org/profile/hcard',
+ 'http://microformats.org/profile/hcalendar#vevent',
+ 'http://n.whatwg.org/work',
+ );
+ if ( isset( $out['itemtype'] ) && !in_array( $out['itemtype'],
+ $allowedTypes ) ) {
+ # Kill everything
+ unset( $out['itemscope'] );
+ }
+ # itemtype, itemid, itemref don't make sense without itemscope
+ if ( !array_key_exists( 'itemscope', $out ) ) {
+ unset( $out['itemtype'] );
+ unset( $out['itemid'] );
+ unset( $out['itemref'] );
+ }
+ # TODO: Strip itemprop if we aren't descendants of an itemscope.
+ }
return $out;
}
@@ -628,8 +708,8 @@ class Sanitizer {
* will be combined (if they're both strings).
*
* @todo implement merging for other attributes such as style
- * @param array $a
- * @param array $b
+ * @param $a Array
+ * @param $b Array
* @return array
*/
static function mergeAttributes( $a, $b ) {
@@ -650,8 +730,8 @@ class Sanitizer {
*
* Currently URL references, 'expression', 'tps' are forbidden.
*
- * @param string $value
- * @return mixed
+ * @param $value String
+ * @return Mixed
*/
static function checkCss( $value ) {
$value = Sanitizer::decodeCharReferences( $value );
@@ -722,9 +802,9 @@ class Sanitizer {
* - Unsafe style attributes are discarded
* - Prepends space if there are attributes.
*
- * @param string $text
- * @param string $element
- * @return string
+ * @param $text String
+ * @param $element String
+ * @return String
*/
static function fixTagAttributes( $text, $element ) {
if( trim( $text ) == '' ) {
@@ -746,7 +826,7 @@ class Sanitizer {
/**
* Encode an attribute value for HTML output.
- * @param $text
+ * @param $text String
* @return HTML-encoded text fragment
*/
static function encodeAttribute( $text ) {
@@ -767,7 +847,7 @@ class Sanitizer {
/**
* Encode an attribute value for HTML tags, with extra armoring
* against further wiki processing.
- * @param $text
+ * @param $text String
* @return HTML-encoded text fragment
*/
static function safeEncodeAttribute( $text ) {
@@ -798,63 +878,64 @@ class Sanitizer {
}
/**
- * Given a value escape it so that it can be used in an id attribute and
- * return it, this does not validate the value however (see first link)
+ * Given a value, escape it so that it can be used in an id attribute and
+ * return it. This will use HTML5 validation if $wgExperimentalHtmlIds is
+ * true, allowing anything but ASCII whitespace. Otherwise it will use
+ * HTML 4 rules, which means a narrow subset of ASCII, with bad characters
+ * escaped with lots of dots.
+ *
+ * To ensure we don't have to bother escaping anything, we also strip ', ",
+ * & even if $wgExperimentalIds is true. TODO: Is this the best tactic?
+ * We also strip # because it upsets IE6.
*
* @see http://www.w3.org/TR/html401/types.html#type-name Valid characters
* in the id and
* name attributes
* @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
+ * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-id-attribute
+ * HTML5 definition of id attribute
*
- * @param string $id Id to validate
- * @param mixed $options String or array of strings (default is array()):
+ * @param $id String: id to escape
+ * @param $options Mixed: string or array of strings (default is array()):
* 'noninitial': This is a non-initial fragment of an id, not a full id,
* so don't pay attention if the first character isn't valid at the
- * beginning of an id.
- * 'xml': Don't restrict the id to be HTML4-compatible. This option
- * allows any alphabetic character to be used, per the XML standard.
- * Therefore, it also completely changes the type of escaping: instead
- * of weird dot-encoding, runs of invalid characters (mostly
- * whitespace) are just compressed into a single underscore.
- * @return string
+ * beginning of an id. Only matters if $wgExperimentalHtmlIds is
+ * false.
+ * 'legacy': Behave the way the old HTML 4-based ID escaping worked even
+ * if $wgExperimentalHtmlIds is used, so we can generate extra
+ * anchors and links won't break.
+ * @return String
*/
static function escapeId( $id, $options = array() ) {
+ global $wgHtml5, $wgExperimentalHtmlIds;
$options = (array)$options;
- if ( !in_array( 'xml', $options ) ) {
- # HTML4-style escaping
- static $replace = array(
- '%3A' => ':',
- '%' => '.'
- );
-
- $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
- $id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
-
- if ( !preg_match( '/^[a-zA-Z]/', $id )
- && !in_array( 'noninitial', $options ) ) {
- // Initial character must be a letter!
- $id = "x$id";
+ if ( $wgHtml5 && $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
+ $id = Sanitizer::decodeCharReferences( $id );
+ $id = preg_replace( '/[ \t\n\r\f_\'"&#]+/', '_', $id );
+ $id = trim( $id, '_' );
+ if ( $id === '' ) {
+ # Must have been all whitespace to start with.
+ return '_';
+ } else {
+ return $id;
}
- return $id;
}
- # XML-style escaping. For the patterns used, see the XML 1.0 standard,
- # 5th edition, NameStartChar and NameChar: <http://www.w3.org/TR/REC-xml/>
- $nameStartChar = ':a-zA-Z_\xC0-\xD6\xD8-\xF6\xF8-\x{2FF}\x{370}-\x{37D}'
- . '\x{37F}-\x{1FFF}\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}'
- . '\x{3001}-\x{D7FF}\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}';
- $nameChar = $nameStartChar . '.\-0-9\xB7\x{0300}-\x{036F}'
- . '\x{203F}-\x{2040}';
- # Replace _ as well so we don't get multiple consecutive underscores
- $id = preg_replace( "/([^$nameChar]|_)+/u", '_', $id );
- $id = trim( $id, '_' );
-
- if ( !preg_match( "/^[$nameStartChar]/u", $id )
- && !in_array( 'noninitial', $options ) ) {
- $id = "_$id";
- }
+ # HTML4-style escaping
+ static $replace = array(
+ '%3A' => ':',
+ '%' => '.'
+ );
+
+ $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
+ $id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
+ if ( !preg_match( '/^[a-zA-Z]/', $id )
+ && !in_array( 'noninitial', $options ) ) {
+ // Initial character must be a letter!
+ $id = "x$id";
+ }
return $id;
}
@@ -866,8 +947,8 @@ class Sanitizer {
*
* @see http://www.w3.org/TR/CSS21/syndata.html Valid characters/format
*
- * @param string $class
- * @return string
+ * @param $class String
+ * @return String
*/
static function escapeClass( $class ) {
// Convert ugly stuff to underscores and kill underscores in ugly places
@@ -881,8 +962,8 @@ class Sanitizer {
* Given HTML input, escape with htmlspecialchars but un-escape entites.
* This allows (generally harmless) entities like &nbsp; to survive.
*
- * @param string $html String to escape
- * @return string Escaped input
+ * @param $html String to escape
+ * @return String: escaped input
*/
static function escapeHtmlAllowEntities( $html ) {
# It seems wise to escape ' as well as ", as a matter of course. Can't
@@ -895,9 +976,8 @@ class Sanitizer {
/**
* Regex replace callback for armoring links against further processing.
- * @param array $matches
+ * @param $matches Array
* @return string
- * @private
*/
private static function armorLinksCallback( $matches ) {
return str_replace( ':', '&#58;', $matches[1] );
@@ -908,16 +988,15 @@ class Sanitizer {
* a partial tag string. Attribute names are forces to lowercase,
* character references are decoded to UTF-8 text.
*
- * @param string
- * @return array
+ * @param $text String
+ * @return Array
*/
public static function decodeTagAttributes( $text ) {
- $attribs = array();
-
if( trim( $text ) == '' ) {
- return $attribs;
+ return array();
}
+ $attribs = array();
$pairs = array();
if( !preg_match_all(
MW_ATTRIBS_REGEX,
@@ -945,9 +1024,8 @@ class Sanitizer {
* Pick the appropriate attribute value from a match set from the
* MW_ATTRIBS_REGEX matches.
*
- * @param array $set
- * @return string
- * @private
+ * @param $set Array
+ * @return String
*/
private static function getTagAttributeCallback( $set ) {
if( isset( $set[6] ) ) {
@@ -979,9 +1057,8 @@ class Sanitizer {
* but note that we're not returning the value, but are returning
* XML source fragments that will be slapped into output.
*
- * @param string $text
- * @return string
- * @private
+ * @param $text String
+ * @return String
*/
private static function normalizeAttributeValue( $text ) {
return str_replace( '"', '&quot;',
@@ -1006,8 +1083,8 @@ class Sanitizer {
* c. use &#x, not &#X
* d. fix or reject non-valid attributes
*
- * @param string $text
- * @return string
+ * @param $text String
+ * @return String
* @private
*/
static function normalizeCharReferences( $text ) {
@@ -1017,8 +1094,8 @@ class Sanitizer {
$text );
}
/**
- * @param string $matches
- * @return string
+ * @param $matches String
+ * @return String
*/
static function normalizeCharReferencesCallback( $matches ) {
$ret = null;
@@ -1044,9 +1121,8 @@ class Sanitizer {
* MediaWiki-specific alias, returns the HTML equivalent. Otherwise,
* returns HTML-escaped text of pseudo-entity source (eg &amp;foo;)
*
- * @param string $name
- * @return string
- * @static
+ * @param $name String
+ * @return String
*/
static function normalizeEntity( $name ) {
global $wgHtmlEntities, $wgHtmlEntityAliases;
@@ -1079,8 +1155,8 @@ class Sanitizer {
/**
* Returns true if a given Unicode codepoint is a valid character in XML.
- * @param int $codepoint
- * @return bool
+ * @param $codepoint Integer
+ * @return Boolean
*/
private static function validateCodepoint( $codepoint ) {
return ($codepoint == 0x09)
@@ -1095,10 +1171,8 @@ class Sanitizer {
* Decode any character references, numeric or named entities,
* in the text and return a UTF-8 string.
*
- * @param string $text
- * @return string
- * @public
- * @static
+ * @param $text String
+ * @return String
*/
public static function decodeCharReferences( $text ) {
return preg_replace_callback(
@@ -1108,8 +1182,8 @@ class Sanitizer {
}
/**
- * @param string $matches
- * @return string
+ * @param $matches String
+ * @return String
*/
static function decodeCharReferencesCallback( $matches ) {
if( $matches[1] != '' ) {
@@ -1128,8 +1202,8 @@ class Sanitizer {
/**
* Return UTF-8 string for a codepoint if that is a valid
* character reference, otherwise U+FFFD REPLACEMENT CHARACTER.
- * @param int $codepoint
- * @return string
+ * @param $codepoint Integer
+ * @return String
* @private
*/
static function decodeChar( $codepoint ) {
@@ -1145,8 +1219,8 @@ class Sanitizer {
* return the UTF-8 encoding of that character. Otherwise, returns
* pseudo-entity source (eg &foo;)
*
- * @param string $name
- * @return string
+ * @param $name Strings
+ * @return String
*/
static function decodeEntity( $name ) {
global $wgHtmlEntities, $wgHtmlEntityAliases;
@@ -1161,11 +1235,10 @@ class Sanitizer {
}
/**
- * Fetch the whitelist of acceptable attributes for a given
- * element name.
+ * Fetch the whitelist of acceptable attributes for a given element name.
*
- * @param string $element
- * @return array
+ * @param $element String
+ * @return Array
*/
static function attributeWhitelist( $element ) {
static $list;
@@ -1180,10 +1253,27 @@ class Sanitizer {
/**
* Foreach array key (an allowed HTML element), return an array
* of allowed attributes
- * @return array
+ * @return Array
*/
static function setupAttributeWhitelist() {
+ global $wgAllowRdfaAttributes, $wgHtml5, $wgAllowMicrodataAttributes;
+
$common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' );
+
+ if ( $wgAllowRdfaAttributes ) {
+ #RDFa attributes as specified in section 9 of http://www.w3.org/TR/2008/REC-rdfa-syntax-20081014
+ $common = array_merge( $common, array(
+ 'about', 'property', 'resource', 'datatype', 'typeof',
+ ) );
+ }
+
+ if ( $wgHtml5 && $wgAllowMicrodataAttributes ) {
+ # add HTML5 microdata tages as pecified by http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-microdata-model
+ $common = array_merge( $common, array(
+ 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype'
+ ) );
+ }
+
$block = array_merge( $common, array( 'align' ) );
$tablealign = array( 'align', 'char', 'charoff', 'valign' );
$tablecell = array( 'abbr',
@@ -1229,7 +1319,7 @@ class Sanitizer {
# samp
# kbd
'var' => $common,
- # abbr
+ 'abbr' => $common,
# acronym
# 9.2.2
@@ -1289,6 +1379,9 @@ class Sanitizer {
'td' => array_merge( $common, $tablecell, $tablealign ),
'th' => array_merge( $common, $tablecell, $tablealign ),
+ # 12.2 # NOTE: <a> is not allowed directly, but the attrib whitelist is used from the Parser object
+ 'a' => array_merge( $common, array( 'href', 'rel', 'rev' ) ), # rel/rev esp. for RDFa
+
# 13.2
# Not usually allowed, but may be used for extension-style hooks
# such as <math> when it is rasterized
@@ -1335,8 +1428,8 @@ class Sanitizer {
* Warning: this return value must be further escaped for literal
* inclusion in HTML output as of 1.10!
*
- * @param string $text HTML fragment
- * @return string
+ * @param $text String: HTML fragment
+ * @return String
*/
static function stripAllTags( $text ) {
# Actual <tags>
@@ -1356,8 +1449,7 @@ class Sanitizer {
*
* Use for passing XHTML fragments to PHP's XML parsing functions
*
- * @return string
- * @static
+ * @return String
*/
static function hackDocType() {
global $wgHtmlEntities;
@@ -1403,7 +1495,7 @@ class Sanitizer {
$host = preg_replace( $strip, '', $host );
- // @fixme: validate hostnames here
+ // @todo Fixme: validate hostnames here
return $protocol . $host . $rest;
} else {
diff --git a/includes/SearchMySQL.php b/includes/SearchMySQL.php
deleted file mode 100644
index 5fc06790..00000000
--- a/includes/SearchMySQL.php
+++ /dev/null
@@ -1,270 +0,0 @@
-<?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
-
-/**
- * @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;
- }
-
- /**
- * Parse the user's query and transform it into an SQL fragment which will
- * become part of a WHERE clause
- */
- function parseQuery( $filteredText, $fulltext ) {
- global $wgContLang;
- $lc = SearchEngine::legalSearchChars(); // Minus format chars
- $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 = "\b$regexp"; // foo*
- } else {
- $regexp = "\b$regexp\b";
- }
- } 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.
- *
- * @param string $term - Raw search term
- * @return MySQLSearchResultSet
- * @access public
- */
- function searchText( $term ) {
- $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), true ) ) );
- return new MySQLSearchResultSet( $resultSet, $this->searchTerms );
- }
-
- /**
- * Perform a title-only search query and return a result set.
- *
- * @param string $term - Raw search term
- * @return MySQLSearchResultSet
- * @access public
- */
- function searchTitle( $term ) {
- $resultSet = $this->db->resultObject( $this->db->query( $this->getQuery( $this->filter( $term ), false ) ) );
- return new MySQLSearchResultSet( $resultSet, $this->searchTerms );
- }
-
-
- /**
- * Return a partial WHERE clause to exclude redirects, if so set
- * @return string
- * @private
- */
- function queryRedirect() {
- if( $this->showRedirects ) {
- return '';
- } else {
- return 'AND page_is_redirect=0';
- }
- }
-
- /**
- * Return a partial WHERE clause to limit the search to the given namespaces
- * @return string
- * @private
- */
- function queryNamespaces() {
- if( is_null($this->namespaces) )
- return ''; # search all
- if ( !count( $this->namespaces ) ) {
- $namespaces = '0';
- } else {
- $namespaces = $this->db->makeList( $this->namespaces );
- }
- return 'AND page_namespace IN (' . $namespaces . ')';
- }
-
- /**
- * Return a LIMIT clause to limit results on the query.
- * @return string
- * @private
- */
- function queryLimit() {
- return $this->db->limitResult( '', $this->limit, $this->offset );
- }
-
- /**
- * Does not do anything for generic search engine
- * subclasses may define this though
- * @return string
- * @private
- */
- function queryRanking( $filteredTerm, $fulltext ) {
- return '';
- }
-
- /**
- * Construct the full SQL query to do the search.
- * The guts shoulds be constructed in queryMain()
- * @param string $filteredTerm
- * @param bool $fulltext
- * @private
- */
- function getQuery( $filteredTerm, $fulltext ) {
- return $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
- $this->queryRedirect() . ' ' .
- $this->queryNamespaces() . ' ' .
- $this->queryRanking( $filteredTerm, $fulltext ) . ' ' .
- $this->queryLimit();
- }
-
-
- /**
- * Picks which field to index on, depending on what type of query.
- * @param bool $fulltext
- * @return string
- */
- function getIndexField( $fulltext ) {
- return $fulltext ? 'si_text' : 'si_title';
- }
-
- /**
- * Get the base part of the search query.
- * The actual match syntax will depend on the server
- * version; MySQL 3 and MySQL 4 have different capabilities
- * in their fulltext search indexes.
- *
- * @param string $filteredTerm
- * @param bool $fulltext
- * @return string
- * @private
- */
- function queryMain( $filteredTerm, $fulltext ) {
- $match = $this->parseQuery( $filteredTerm, $fulltext );
- $page = $this->db->tableName( 'page' );
- $searchindex = $this->db->tableName( 'searchindex' );
- return 'SELECT page_id, page_namespace, page_title ' .
- "FROM $page,$searchindex " .
- 'WHERE page_id=si_page AND ' . $match;
- }
-
- /**
- * Create or update the search index record for the given page.
- * Title and text should be pre-processed.
- *
- * @param int $id
- * @param string $title
- * @param string $text
- */
- function update( $id, $title, $text ) {
- $dbw = wfGetDB( DB_MASTER );
- $dbw->replace( 'searchindex',
- array( 'si_page' ),
- array(
- 'si_page' => $id,
- 'si_title' => $title,
- 'si_text' => $text
- ), __METHOD__ );
- }
-
- /**
- * Update a search index record's title only.
- * Title should be pre-processed.
- *
- * @param int $id
- * @param string $title
- */
- function updateTitle( $id, $title ) {
- $dbw = wfGetDB( DB_MASTER );
-
- $dbw->update( 'searchindex',
- array( 'si_title' => $title ),
- array( 'si_page' => $id ),
- __METHOD__,
- array( $dbw->lowPriorityOption() ) );
- }
-}
-
-/**
- * @ingroup Search
- */
-class MySQLSearchResultSet extends SearchResultSet {
- function MySQLSearchResultSet( $resultSet, $terms ) {
- $this->mResultSet = $resultSet;
- $this->mTerms = $terms;
- }
-
- function termMatches() {
- return $this->mTerms;
- }
-
- function numRows() {
- return $this->mResultSet->numRows();
- }
-
- function next() {
- $row = $this->mResultSet->fetchObject();
- if( $row === false ) {
- return false;
- } else {
- return new SearchResult( $row );
- }
- }
-
- function free() {
- $this->mResultSet->free();
- }
-}
diff --git a/includes/Setup.php b/includes/Setup.php
index d450dfdb..cd9146ab 100644
--- a/includes/Setup.php
+++ b/includes/Setup.php
@@ -8,7 +8,6 @@
* MEDIAWIKI is defined
*/
if( !defined( 'MEDIAWIKI' ) ) {
- echo "This file is part of MediaWiki, it is not a valid entry point.\n";
exit( 1 );
}
@@ -41,6 +40,7 @@ if( $wgArticlePath === false ) {
if( $wgStylePath === false ) $wgStylePath = "$wgScriptPath/skins";
if( $wgStyleDirectory === false) $wgStyleDirectory = "$IP/skins";
+if( $wgExtensionAssetsPath === false ) $wgExtensionAssetsPath = "$wgScriptPath/extensions";
if( $wgLogo === false ) $wgLogo = "$wgStylePath/common/images/wiki.png";
@@ -59,10 +59,10 @@ if ( empty( $wgFileStore['deleted']['directory'] ) ) {
}
/**
- * Unconditional protection for NS_MEDIAWIKI since otherwise it's too easy for a
- * sysadmin to set $wgNamespaceProtection incorrectly and leave the wiki insecure.
+ * Unconditional protection for NS_MEDIAWIKI since otherwise it's too easy for a
+ * sysadmin to set $wgNamespaceProtection incorrectly and leave the wiki insecure.
*
- * Note that this is the definition of editinterface and it can be granted to
+ * Note that this is the definition of editinterface and it can be granted to
* all users if desired.
*/
$wgNamespaceProtection[NS_MEDIAWIKI] = 'editinterface';
@@ -87,7 +87,6 @@ if ( !$wgLocalFileRepo ) {
'hashLevels' => $wgHashedUploadDirectory ? 2 : 0,
'thumbScriptUrl' => $wgThumbnailScriptPath,
'transformVia404' => !$wgGenerateThumbnailOnParse,
- 'initialCapital' => $wgCapitalLinks,
'deletedDir' => $wgFileStore['deleted']['directory'],
'deletedHashLevels' => $wgFileStore['deleted']['hash']
);
@@ -130,6 +129,18 @@ if ( $wgUseSharedUploads ) {
);
}
}
+if( $wgUseInstantCommons ) {
+ $wgForeignFileRepos[] = array(
+ 'class' => 'ForeignAPIRepo',
+ 'name' => 'wikimediacommons',
+ 'apibase' => 'http://commons.wikimedia.org/w/api.php',
+ 'hashLevels' => 2,
+ 'fetchDescription' => true,
+ 'descriptionCacheExpiry' => 43200,
+ 'apiThumbCacheExpiry' => 86400,
+ );
+}
+
if ( !class_exists( 'AutoLoader' ) ) {
require_once( "$IP/includes/AutoLoader.php" );
}
@@ -150,6 +161,17 @@ require_once( "$IP/includes/StubObject.php" );
wfProfileOut( $fname.'-includes' );
wfProfileIn( $fname.'-misc1' );
+# Raise the memory limit if it's too low
+wfMemoryLimit();
+
+/**
+ * Set up the timezone, suppressing the pseudo-security warning in PHP 5.1+
+ * that happens whenever you use a date function without the timezone being
+ * explicitly set. Inspired by phpMyAdmin's treatment of the problem.
+ */
+wfSuppressWarnings();
+date_default_timezone_set( date_default_timezone_get() );
+wfRestoreWarnings();
$wgIP = false; # Load on demand
# Can't stub this one, it sets up $_GET and $_REQUEST in its constructor
@@ -158,16 +180,28 @@ $wgRequest = new WebRequest;
# Useful debug output
if ( $wgCommandLineMode ) {
wfDebug( "\n\nStart command line script $self\n" );
-} elseif ( function_exists( 'getallheaders' ) ) {
- wfDebug( "\n\nStart request\n" );
+} else {
+ wfDebug( "Start request\n\n" );
wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" );
- $headers = getallheaders();
- foreach ($headers as $name => $value) {
- wfDebug( "$name: $value\n" );
+
+ if ( $wgDebugPrintHttpHeaders ) {
+ $headerOut = "HTTP HEADERS:\n";
+
+ if ( function_exists( 'getallheaders' ) ) {
+ $headers = getallheaders();
+ foreach ( $headers as $name => $value ) {
+ $headerOut .= "$name: $value\n";
+ }
+ } else {
+ $headers = $_SERVER;
+ foreach ( $headers as $name => $value ) {
+ if ( substr( $name, 0, 5 ) !== 'HTTP_' ) continue;
+ $name = substr( $name, 5 );
+ $headerOut .= "$name: $value\n";
+ }
+ }
+ wfDebug( "$headerOut\n" );
}
- wfDebug( "\n" );
-} elseif( isset( $_SERVER['REQUEST_URI'] ) ) {
- wfDebug( $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n" );
}
if( $wgRCFilterByAge ) {
@@ -201,6 +235,23 @@ $wgContLanguageCode = $wgLanguageCode;
# If file cache or squid cache is on, just disable this (DWIMD).
if( $wgUseFileCache || $wgUseSquid ) $wgShowIPinHeader = false;
+# $wgAllowRealName and $wgAllowUserSkin were removed in 1.16
+# in favor of $wgHiddenPrefs, handle b/c here
+if( !$wgAllowRealName ) {
+ $wgHiddenPrefs[] = 'realname';
+}
+
+if( !$wgAllowUserSkin ) {
+ $wgHiddenPrefs[] = 'skin';
+}
+
+if ( !$wgHtml5Version && $wgHtml5 && $wgAllowRdfaAttributes ) {
+ # see http://www.w3.org/TR/rdfa-in-html/#document-conformance
+ if ( $wgMimeType == 'application/xhtml+xml' ) $wgHtml5Version = 'XHTML+RDFa 1.0';
+ else $wgHtml5Version = 'HTML+RDFa 1.0';
+}
+
+
wfProfileOut( $fname.'-misc1' );
wfProfileIn( $fname.'-memcached' );
@@ -208,9 +259,9 @@ $wgMemc =& wfGetMainCache();
$messageMemc =& wfGetMessageCacheStorage();
$parserMemc =& wfGetParserCacheStorage();
-wfDebug( 'Main cache: ' . get_class( $wgMemc ) .
- "\nMessage cache: " . get_class( $messageMemc ) .
- "\nParser cache: " . get_class( $parserMemc ) . "\n" );
+wfDebug( 'CACHES: ' . get_class( $wgMemc ) . '[main] ' .
+ get_class( $messageMemc ) . '[message] ' .
+ get_class( $parserMemc ) . "[parser]\n" );
wfProfileOut( $fname.'-memcached' );
@@ -289,9 +340,7 @@ $wgDeferredUpdateList = array();
$wgPostCommitUpdateList = array();
if ( $wgAjaxWatch ) $wgAjaxExportList[] = 'wfAjaxWatch';
-if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'UploadForm::ajaxGetExistsWarning';
-if( $wgAjaxLicensePreview )
- $wgAjaxExportList[] = 'UploadForm::ajaxGetLicensePreview';
+if ( $wgAjaxUploadDestCheck ) $wgAjaxExportList[] = 'SpecialUpload::ajaxGetExistsWarning';
# Placeholders in case of DB error
$wgTitle = null;
@@ -305,7 +354,18 @@ wfProfileIn( $fname.'-extensions' );
# of the extension file. This allows the extension to perform
# any necessary initialisation in the fully initialised environment
foreach ( $wgExtensionFunctions as $func ) {
- $profName = $fname.'-extensions-'.strval( $func );
+ # Allow closures in PHP 5.3+
+ if ( is_object( $func ) && $func instanceof Closure ) {
+ $profName = $fname.'-extensions-closure';
+ } elseif( is_array( $func ) ) {
+ if ( is_object( $func[0] ) )
+ $profName = $fname.'-extensions-'.get_class( $func[0] ).'::'.$func[1];
+ else
+ $profName = $fname.'-extensions-'.implode( '::', $func );
+ } else {
+ $profName = $fname.'-extensions-'.strval( $func );
+ }
+
wfProfileIn( $profName );
call_user_func( $func );
wfProfileOut( $profName );
diff --git a/includes/SiteConfiguration.php b/includes/SiteConfiguration.php
index 2ed28139..b6d83670 100644
--- a/includes/SiteConfiguration.php
+++ b/includes/SiteConfiguration.php
@@ -36,6 +36,14 @@ class SiteConfiguration {
* Array of domains that are local and can be handled by the same server
*/
public $localVHosts = array();
+
+ /**
+ * Optional callback to load full configuration data.
+ */
+ public $fullLoadCallback = null;
+
+ /** Whether or not all data has been loaded */
+ public $fullLoadDone = false;
/**
* A callback function that returns an array with the following keys (all
@@ -387,5 +395,12 @@ class SiteConfiguration {
return $out;
}
+
+ public function loadFullData() {
+ if ($this->fullLoadCallback && !$this->fullLoadDone) {
+ call_user_func( $this->fullLoadCallback, $this );
+ $this->fullLoadDone = true;
+ }
+ }
}
-}
+} // End of multiple inclusion guard
diff --git a/includes/SiteStats.php b/includes/SiteStats.php
index 9427536f..16e3c5f2 100644
--- a/includes/SiteStats.php
+++ b/includes/SiteStats.php
@@ -49,12 +49,7 @@ class SiteStats {
// 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();
+ SiteStatsInit::doAllAndCommit( false );
$row = self::doLoad( wfGetDB( DB_MASTER ) );
}
@@ -93,7 +88,7 @@ class SiteStats {
self::load();
return self::$row->ss_users;
}
-
+
static function activeUsers() {
self::load();
return self::$row->ss_active_users;
@@ -111,7 +106,7 @@ class SiteStats {
wfDeprecated(__METHOD__);
return self::numberingroup('sysop');
}
-
+
/**
* Find the number of users in a given user group.
* @param string $group Name of group
@@ -124,13 +119,13 @@ class SiteStats {
$hit = $wgMemc->get( $key );
if ( !$hit ) {
$dbr = wfGetDB( DB_SLAVE );
- $hit = $dbr->selectField( 'user_groups', 'COUNT(*)',
- array( 'ug_group' => $group ), __METHOD__ );
+ $hit = $dbr->selectField( 'user_groups', 'COUNT(*)',
+ array( 'ug_group' => $group ), __METHOD__ );
$wgMemc->set( $key, $hit, 3600 );
}
self::$groupMemberCounts[$group] = $hit;
}
- return self::$groupMemberCounts[$group];
+ return self::$groupMemberCounts[$group];
}
static function jobs() {
@@ -209,7 +204,6 @@ class SiteStatsUpdate {
}
function doUpdate() {
- $fname = 'SiteStatsUpdate::doUpdate';
$dbw = wfGetDB( DB_MASTER );
$updates = '';
@@ -226,11 +220,11 @@ class SiteStatsUpdate {
# Need a separate transaction because this a global lock
$dbw->begin();
- $dbw->query( $sql, $fname );
+ $dbw->query( $sql, __METHOD__ );
$dbw->commit();
}
}
-
+
public static function cacheUpdate( $dbw ) {
$dbr = wfGetDB( DB_SLAVE, array( 'SpecialStatistics', 'vslow') );
# Get non-bot users than did some recent action other than making accounts.
@@ -238,9 +232,153 @@ class SiteStatsUpdate {
$activeUsers = $dbr->selectField( 'recentchanges', 'COUNT( DISTINCT rc_user_text )',
array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers' OR rc_log_type IS NULL" ),
__METHOD__ );
- $dbw->update( 'site_stats',
+ $dbw->update( 'site_stats',
array( 'ss_active_users' => intval($activeUsers) ),
array( 'ss_row_id' => 1 ), __METHOD__
);
+ return $activeUsers;
+ }
+}
+
+/**
+ * Class designed for counting of stats.
+ */
+class SiteStatsInit {
+
+ // Db connection
+ private $db;
+
+ // Various stats
+ private $mEdits, $mArticles, $mPages, $mUsers, $mViews, $mFiles = 0;
+
+ /**
+ * Constructor
+ * @param $useMaster bool Whether to use the master db
+ */
+ public function __construct( $useMaster = false ) {
+ $this->db = wfGetDB( $useMaster ? DB_MASTER : DB_SLAVE );
+ }
+
+ /**
+ * Count the total number of edits
+ * @return int
+ */
+ public function edits() {
+ $this->mEdits = $this->db->selectField( 'revision', 'COUNT(*)', '', __METHOD__ );
+ $this->mEdits += $this->db->selectField( 'archive', 'COUNT(*)', '', __METHOD__ );
+ return $this->mEdits;
+ }
+
+ /**
+ * Count pages in article space
+ * @return int
+ */
+ public function articles() {
+ global $wgContentNamespaces;
+ $this->mArticles = $this->db->selectField( 'page', 'COUNT(*)', array( 'page_namespace' => $wgContentNamespaces, 'page_is_redirect' => 0, 'page_len > 0' ), __METHOD__ );
+ return $this->mArticles;
+ }
+
+ /**
+ * Count total pages
+ * @return int
+ */
+ public function pages() {
+ $this->mPages = $this->db->selectField( 'page', 'COUNT(*)', '', __METHOD__ );
+ return $this->mPages;
+ }
+
+ /**
+ * Count total users
+ * @return int
+ */
+ public function users() {
+ $this->mUsers = $this->db->selectField( 'user', 'COUNT(*)', '', __METHOD__ );
+ return $this->mUsers;
+ }
+
+ /**
+ * Count views
+ * @return int
+ */
+ public function views() {
+ $this->mViews = $this->db->selectField( 'page', 'SUM(page_counter)', '', __METHOD__ );
+ return $this->mViews;
+ }
+
+ /**
+ * Count total files
+ * @return int
+ */
+ public function files() {
+ $this->mFiles = $this->db->selectField( 'image', 'COUNT(*)', '', __METHOD__ );
+ return $this->mFiles;
+ }
+
+ /**
+ * Do all updates and commit them. More or less a replacement
+ * for the original initStats, but without the calls to wfOut()
+ * @param $update bool Whether to update the current stats or write fresh
+ * @param $noViews bool When true, do not update the number of page views
+ * @param $activeUsers Whether to update the number of active users
+ */
+ public static function doAllAndCommit( $update, $noViews = false, $activeUsers = false ) {
+ // Grab the object and count everything
+ $counter = new SiteStatsInit( false );
+ $counter->edits();
+ $counter->articles();
+ $counter->pages();
+ $counter->users();
+ $counter->files();
+
+ // Only do views if we don't want to not count them
+ if( !$noViews )
+ $counter->views();
+
+ // Update/refresh
+ if( $update )
+ $counter->update();
+ else
+ $counter->refresh();
+
+ // Count active users if need be
+ if( $activeUsers )
+ SiteStatsUpdate::cacheUpdate( wfGetDB( DB_MASTER ) );
+ }
+
+ /**
+ * Update the current row with the selected values
+ */
+ public function update() {
+ list( $values, $conds ) = $this->getDbParams();
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'site_stats', $values, $conds, __METHOD__ );
+ }
+
+ /**
+ * Refresh site_stats. Erase the current record and save all
+ * the new values.
+ */
+ public function refresh() {
+ list( $values, $conds, $views ) = $this->getDbParams();
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'site_stats', $conds, __METHOD__ );
+ $dbw->insert( 'site_stats', array_merge( $values, $conds, $views ), __METHOD__ );
+ }
+
+ /**
+ * Return three arrays of params for the db queries
+ * @return array
+ */
+ private function getDbParams() {
+ $values = array( 'ss_total_edits' => $this->mEdits,
+ 'ss_good_articles' => $this->mArticles,
+ 'ss_total_pages' => $this->mPages,
+ 'ss_users' => $this->mUsers,
+ 'ss_admins' => SiteStats::numberingroup( 'sysop' ), // @todo make this go away
+ 'ss_images' => $this->mFiles );
+ $conds = array( 'ss_row_id' => 1 );
+ $views = array( 'ss_total_views' => $this->mViews );
+ return array( $values, $conds, $views );
}
}
diff --git a/includes/Skin.php b/includes/Skin.php
index 47285acc..d1a0016d 100644
--- a/includes/Skin.php
+++ b/includes/Skin.php
@@ -3,8 +3,9 @@
* @defgroup Skins Skins
*/
-if ( ! defined( 'MEDIAWIKI' ) )
+if ( !defined( 'MEDIAWIKI' ) ) {
die( 1 );
+}
/**
* The main skin class that provide methods and properties for all other skins.
@@ -23,15 +24,18 @@ class Skin extends Linker {
protected $searchboxes = '';
/**#@-*/
protected $mRevisionId; // The revision ID we're looking at, null if not applicable.
- protected $skinname = 'standard' ;
+ protected $skinname = 'standard';
+ // @todo Fixme: should be protected :-\
+ var $mTitle = null;
/** Constructor, call parent constructor */
- function Skin() { parent::__construct(); }
+ function __construct() {
+ parent::__construct();
+ }
/**
* Fetch the set of available skins.
* @return array of strings
- * @static
*/
static function getSkinNames() {
global $wgValidSkinNames;
@@ -46,12 +50,12 @@ class Skin extends Linker {
$skinDir = dir( $wgStyleDirectory );
# while code from www.php.net
- while (false !== ($file = $skinDir->read())) {
+ while( false !== ( $file = $skinDir->read() ) ) {
// Skip non-PHP files, hidden files, and '.dep' includes
$matches = array();
- if(preg_match('/^([^.]*)\.php$/',$file, $matches)) {
+ if( preg_match( '/^([^.]*)\.php$/', $file, $matches ) ) {
$aSkin = $matches[1];
- $wgValidSkinNames[strtolower($aSkin)] = $aSkin;
+ $wgValidSkinNames[strtolower( $aSkin )] = $aSkin;
}
}
$skinDir->close();
@@ -60,7 +64,7 @@ class Skin extends Linker {
}
return $wgValidSkinNames;
}
-
+
/**
* Fetch the list of usable skins in regards to $wgSkipSkins.
* Useful for Special:Preferences and other places where you
@@ -80,9 +84,8 @@ class Skin extends Linker {
* Normalize a skin preference value to a form that can be loaded.
* If a skin can't be found, it will fall back to the configured
* default (or the old 'Classic' skin if that's broken).
- * @param string $key
+ * @param $key String: 'monobook', 'standard', etc.
* @return string
- * @static
*/
static function normalizeKey( $key ) {
global $wgDefaultSkin;
@@ -103,9 +106,10 @@ class Skin extends Linker {
$fallback = array(
0 => $wgDefaultSkin,
1 => 'nostalgia',
- 2 => 'cologneblue' );
+ 2 => 'cologneblue'
+ );
- if( isset( $fallback[$key] ) ){
+ if( isset( $fallback[$key] ) ) {
$key = $fallback[$key];
}
@@ -118,9 +122,8 @@ class Skin extends Linker {
/**
* Factory method for loading a skin of a given type
- * @param string $key 'monobook', 'standard', etc
+ * @param $key String: 'monobook', 'standard', etc.
* @return Skin
- * @static
*/
static function &newFromKey( $key ) {
global $wgStyleDirectory;
@@ -129,13 +132,15 @@ class Skin extends Linker {
$skinNames = Skin::getSkinNames();
$skinName = $skinNames[$key];
- $className = 'Skin'.ucfirst($key);
+ $className = 'Skin' . ucfirst( $key );
# Grab the skin class and initialise it.
if ( !class_exists( $className ) ) {
// Preload base classes to work around APC/PHP5 bug
$deps = "{$wgStyleDirectory}/{$skinName}.deps.php";
- if( file_exists( $deps ) ) include_once( $deps );
+ if( file_exists( $deps ) ) {
+ include_once( $deps );
+ }
require_once( "{$wgStyleDirectory}/{$skinName}.php" );
# Check if we got if not failback to default skin
@@ -166,7 +171,9 @@ class Skin extends Linker {
function qbSetting() {
global $wgOut, $wgUser;
- if ( $wgOut->isQuickbarSuppressed() ) { return 0; }
+ if ( $wgOut->isQuickbarSuppressed() ) {
+ return 0;
+ }
$q = $wgUser->getOption( 'quickbar', 0 );
return $q;
}
@@ -178,7 +185,7 @@ class Skin extends Linker {
# Generally the order of the favicon and apple-touch-icon links
# should not matter, but Konqueror (3.5.9 at least) incorrectly
- # uses whichever one appears later in the HTML source. Make sure
+ # uses whichever one appears later in the HTML source. Make sure
# apple-touch-icon is specified first to avoid this.
if( false !== $wgAppleTouchIcon ) {
$out->addLink( array( 'rel' => 'apple-touch-icon', 'href' => $wgAppleTouchIcon ) );
@@ -196,7 +203,7 @@ class Skin extends Linker {
'title' => wfMsgForContent( 'opensearch-desc' ),
));
- $this->addMetadataLinks($out);
+ $this->addMetadataLinks( $out );
$this->mRevisionId = $out->mRevisionId;
@@ -209,26 +216,31 @@ class Skin extends Linker {
* Preload the existence of three commonly-requested pages in a single query
*/
function preloadExistence() {
- global $wgUser, $wgTitle;
+ global $wgUser;
// User/talk link
$titles = array( $wgUser->getUserPage(), $wgUser->getTalkPage() );
// Other tab link
- if ( $wgTitle->getNamespace() == NS_SPECIAL ) {
+ if ( $this->mTitle->getNamespace() == NS_SPECIAL ) {
// nothing
- } elseif ( $wgTitle->isTalkPage() ) {
- $titles[] = $wgTitle->getSubjectPage();
+ } elseif ( $this->mTitle->isTalkPage() ) {
+ $titles[] = $this->mTitle->getSubjectPage();
} else {
- $titles[] = $wgTitle->getTalkPage();
+ $titles[] = $this->mTitle->getTalkPage();
}
$lb = new LinkBatch( $titles );
$lb->execute();
}
+ /**
+ * Adds metadata links (Creative Commons/Dublin Core/copyright) to the HTML
+ * output.
+ * @param $out Object: instance of OutputPage
+ */
function addMetadataLinks( OutputPage $out ) {
- global $wgTitle, $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf;
+ global $wgEnableDublinCoreRdf, $wgEnableCreativeCommonsRdf;
global $wgRightsPage, $wgRightsUrl;
if( $out->isArticleRelated() ) {
@@ -237,13 +249,15 @@ class Skin extends Linker {
$out->addMetadataLink( array(
'title' => 'Creative Commons',
'type' => 'application/rdf+xml',
- 'href' => $wgTitle->getLocalURL( 'action=creativecommons') ) );
+ 'href' => $this->mTitle->getLocalURL( 'action=creativecommons' ) )
+ );
}
if( $wgEnableDublinCoreRdf ) {
$out->addMetadataLink( array(
'title' => 'Dublin Core',
'type' => 'application/rdf+xml',
- 'href' => $wgTitle->getLocalURL( 'action=dublincore' ) ) );
+ 'href' => $this->mTitle->getLocalURL( 'action=dublincore' ) )
+ );
}
}
$copyright = '';
@@ -259,18 +273,38 @@ class Skin extends Linker {
if( $copyright ) {
$out->addLink( array(
'rel' => 'copyright',
- 'href' => $copyright ) );
+ 'href' => $copyright )
+ );
}
}
- function setMembers(){
- global $wgTitle, $wgUser;
- $this->mTitle = $wgTitle;
+ /**
+ * Set some local variables
+ */
+ protected function setMembers() {
+ global $wgUser;
$this->mUser = $wgUser;
$this->userpage = $wgUser->getUserPage()->getPrefixedText();
$this->usercss = false;
}
+ /**
+ * Set the title
+ * @param Title $t The title to use
+ */
+ public function setTitle( $t ) {
+ $this->mTitle = $t;
+ }
+
+ /** Get the title */
+ public function getTitle() {
+ return $this->mTitle;
+ }
+
+ /**
+ * Outputs the HTML generated by other functions.
+ * @param $out Object: instance of OutputPage
+ */
function outputPage( OutputPage $out ) {
global $wgDebugComments;
wfProfileIn( __METHOD__ );
@@ -283,12 +317,6 @@ class Skin extends Linker {
$out->out( $out->headElement( $this ) );
- $out->out( "\n<body" );
- $ops = $this->getBodyOptions();
- foreach ( $ops as $name => $val ) {
- $out->out( " $name='$val'" );
- }
- $out->out( ">\n" );
if ( $wgDebugComments ) {
$out->out( "<!-- Wiki debugging output:\n" .
$out->mDebugtext . "-->\n" );
@@ -299,7 +327,7 @@ class Skin extends Linker {
$out->out( $out->mBodytext . "\n" );
$out->out( $this->afterContent() );
-
+
$out->out( $afterContent );
$out->out( $this->bottomScripts() );
@@ -311,36 +339,42 @@ class Skin extends Linker {
}
static function makeVariablesScript( $data ) {
- global $wgJsMimeType;
-
- $r = array( "<script type= \"$wgJsMimeType\">/*<![CDATA[*/" );
- foreach ( $data as $name => $value ) {
- $encValue = Xml::encodeJsVar( $value );
- $r[] = "var $name = $encValue;";
+ if( $data ) {
+ $r = array();
+ foreach ( $data as $name => $value ) {
+ $encValue = Xml::encodeJsVar( $value );
+ $r[] = "$name=$encValue";
+ }
+ $js = 'var ' . implode( ",\n", $r ) . ';';
+ return Html::inlineScript( "\n$js\n" );
+ } else {
+ return '';
}
- $r[] = "/*]]>*/</script>\n";
-
- return implode( "\n\t\t", $r );
}
/**
* Make a <script> tag containing global variables
- * @param array $data Associative array containing one element:
- * skinname => the skin name
+ * @param $skinName string Name of the skin
* The odd calling convention is for backwards compatibility
+ * @TODO @FIXME Make this not depend on $wgTitle!
*/
- static function makeGlobalVariablesScript( $data ) {
- global $wgScript, $wgStylePath, $wgUser;
+ static function makeGlobalVariablesScript( $skinName ) {
+ if ( is_array( $skinName ) ) {
+ # Weird back-compat stuff.
+ $skinName = $skinName['skinname'];
+ }
+ global $wgScript, $wgTitle, $wgStylePath, $wgUser, $wgScriptExtension;
global $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgLang;
- global $wgTitle, $wgCanonicalNamespaceNames, $wgOut, $wgArticle;
+ global $wgOut, $wgArticle;
global $wgBreakFrames, $wgRequest, $wgVariantArticlePath, $wgActionPaths;
global $wgUseAjax, $wgAjaxWatch;
global $wgVersion, $wgEnableAPI, $wgEnableWriteAPI;
- global $wgRestrictionTypes, $wgLivePreview;
+ global $wgRestrictionTypes;
global $wgMWSuggestTemplate, $wgDBname, $wgEnableMWSuggest;
+ global $wgSitename;
$ns = $wgTitle->getNamespace();
- $nsname = isset( $wgCanonicalNamespaceNames[ $ns ] ) ? $wgCanonicalNamespaceNames[ $ns ] : $wgTitle->getNsText();
+ $nsname = MWNamespace::exists( $ns ) ? MWNamespace::getCanonicalName( $ns ) : $wgTitle->getNsText();
$separatorTransTable = $wgContLang->separatorTransformTable();
$separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
$compactSeparatorTransTable = array(
@@ -354,25 +388,29 @@ class Skin extends Linker {
implode( "\t", $digitTransTable ),
);
+ $mainPage = Title::newFromText( wfMsgForContent( 'mainpage' ) );
$vars = array(
- 'skin' => $data['skinname'],
+ 'skin' => $skinName,
'stylepath' => $wgStylePath,
+ 'wgUrlProtocols' => wfUrlProtocols(),
'wgArticlePath' => $wgArticlePath,
'wgScriptPath' => $wgScriptPath,
+ 'wgScriptExtension' => $wgScriptExtension,
'wgScript' => $wgScript,
'wgVariantArticlePath' => $wgVariantArticlePath,
'wgActionPaths' => (object)$wgActionPaths,
'wgServer' => $wgServer,
'wgCanonicalNamespace' => $nsname,
- 'wgCanonicalSpecialPageName' => SpecialPage::resolveAlias( $wgTitle->getDBkey() ),
+ 'wgCanonicalSpecialPageName' => $ns == NS_SPECIAL ?
+ SpecialPage::resolveAlias( $wgTitle->getDBkey() ) : false, # bug 21115
'wgNamespaceNumber' => $wgTitle->getNamespace(),
'wgPageName' => $wgTitle->getPrefixedDBKey(),
'wgTitle' => $wgTitle->getText(),
'wgAction' => $wgRequest->getText( 'action', 'view' ),
'wgArticleId' => $wgTitle->getArticleId(),
'wgIsArticle' => $wgOut->isArticle(),
- 'wgUserName' => $wgUser->isAnon() ? NULL : $wgUser->getName(),
- 'wgUserGroups' => $wgUser->isAnon() ? NULL : $wgUser->getEffectiveGroups(),
+ 'wgUserName' => $wgUser->isAnon() ? null : $wgUser->getName(),
+ 'wgUserGroups' => $wgUser->isAnon() ? null : $wgUser->getEffectiveGroups(),
'wgUserLanguage' => $wgLang->getCode(),
'wgContentLanguage' => $wgContLang->getCode(),
'wgBreakFrames' => $wgBreakFrames,
@@ -382,63 +420,48 @@ class Skin extends Linker {
'wgEnableWriteAPI' => $wgEnableWriteAPI,
'wgSeparatorTransformTable' => $compactSeparatorTransTable,
'wgDigitTransformTable' => $compactDigitTransTable,
+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
+ 'wgSiteName' => $wgSitename,
+ 'wgCategories' => $wgOut->getCategories(),
);
-
- if( $wgUseAjax && $wgEnableMWSuggest && !$wgUser->getOption( 'disablesuggest', false )){
+ if ( $wgContLang->hasVariants() ) {
+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
+ }
+
+ // if on upload page output the extension list & js_upload
+ if( SpecialPage::resolveAlias( $wgTitle->getDBkey() ) == 'Upload' ) {
+ global $wgFileExtensions, $wgAjaxUploadInterface;
+ $vars['wgFileExtensions'] = $wgFileExtensions;
+ }
+
+ 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'));
+ $vars['wgMWSuggestMessages'] = array( wfMsg( 'search-mwsuggest-enabled' ), wfMsg( 'search-mwsuggest-disabled' ) );
}
- foreach( $wgRestrictionTypes as $type )
+ foreach( $wgRestrictionTypes as $type ) {
$vars['wgRestriction' . ucfirst( $type )] = $wgTitle->getRestrictions( $type );
-
- if ( $wgLivePreview && $wgUser->getOption( 'uselivepreview' ) ) {
- $vars['wgLivepreviewMessageLoading'] = wfMsg( 'livepreview-loading' );
- $vars['wgLivepreviewMessageReady'] = wfMsg( 'livepreview-ready' );
- $vars['wgLivepreviewMessageFailed'] = wfMsg( 'livepreview-failed' );
- $vars['wgLivepreviewMessageError'] = wfMsg( 'livepreview-error' );
}
if ( $wgOut->isArticleRelated() && $wgUseAjax && $wgAjaxWatch && $wgUser->isLoggedIn() ) {
$msgs = (object)array();
- foreach ( array( 'watch', 'unwatch', 'watching', 'unwatching' ) as $msgName ) {
+ foreach ( array( 'watch', 'unwatch', 'watching', 'unwatching',
+ 'tooltip-ca-watch', 'tooltip-ca-unwatch' ) as $msgName ) {
$msgs->{$msgName . 'Msg'} = wfMsg( $msgName );
}
$vars['wgAjaxWatch'] = $msgs;
}
- wfRunHooks('MakeGlobalVariablesScript', array(&$vars));
+ // Allow extensions to add their custom variables to the global JS variables
+ wfRunHooks( 'MakeGlobalVariablesScript', array( &$vars ) );
return self::makeVariablesScript( $vars );
}
- function getHeadScripts( $allowUserJs ) {
- global $wgStylePath, $wgUser, $wgJsMimeType, $wgStyleVersion;
-
- $vars = self::makeGlobalVariablesScript( array( 'skinname' => $this->getSkinName() ) );
-
- $r = array( "<script type=\"{$wgJsMimeType}\" src=\"{$wgStylePath}/common/wikibits.js?$wgStyleVersion\"></script>" );
- global $wgUseSiteJs;
- if ($wgUseSiteJs) {
- $jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : '';
- $r[] = "<script type=\"$wgJsMimeType\" src=\"".
- htmlspecialchars(self::makeUrl('-',
- "action=raw$jsCache&gen=js&useskin=" .
- urlencode( $this->getSkinName() ) ) ) .
- "\"><!-- site js --></script>";
- }
- if( $allowUserJs && $wgUser->isLoggedIn() ) {
- $userpage = $wgUser->getUserPage();
- $userjs = htmlspecialchars( self::makeUrl(
- $userpage->getPrefixedText().'/'.$this->getSkinName().'.js',
- 'action=raw&ctype='.$wgJsMimeType));
- $r[] = '<script type="'.$wgJsMimeType.'" src="'.$userjs."\"></script>";
- }
- return $vars . "\t\t" . implode ( "\n\t\t", $r );
- }
-
/**
* To make it harder for someone to slip a user a fake
* user-JavaScript or user-CSS preview, a random token
@@ -446,25 +469,30 @@ class Skin extends Linker {
* passed back with the preview request, we won't render
* the code.
*
- * @param string $action
+ * @param $action String: 'edit', 'submit' etc.
* @return bool
- * @private
*/
- function userCanPreview( $action ) {
- global $wgTitle, $wgRequest, $wgUser;
+ public function userCanPreview( $action ) {
+ global $wgRequest, $wgUser;
- if( $action != 'submit' )
+ if( $action != 'submit' ) {
return false;
- if( !$wgRequest->wasPosted() )
+ }
+ if( !$wgRequest->wasPosted() ) {
+ return false;
+ }
+ if( !$this->mTitle->userCanEditCssSubpage() ) {
return false;
- if( !$wgTitle->userCanEditCssJsSubpage() )
+ }
+ if( !$this->mTitle->userCanEditJsSubpage() ) {
return false;
+ }
return $wgUser->matchEditToken(
$wgRequest->getVal( 'wpEditToken' ) );
}
/**
- * generated JavaScript action=raw&gen=js
+ * Generated JavaScript action=raw&gen=js
* This returns MediaWiki:Common.js and MediaWiki:[Skinname].js concate-
* nated together. For some bizarre reason, it does *not* return any
* custom user JS from subpages. Huh?
@@ -474,27 +502,31 @@ class Skin extends Linker {
* top. For now Monobook.js will be maintained, but it should be consi-
* dered deprecated.
*
+ * @param $skinName String: If set, overrides the skin name
* @return string
*/
- public function generateUserJs() {
+ public function generateUserJs( $skinName = null ) {
global $wgStylePath;
wfProfileIn( __METHOD__ );
+ if( !$skinName ) {
+ $skinName = $this->getSkinName();
+ }
$s = "/* generated javascript */\n";
- $s .= "var skin = '" . Xml::escapeJsString( $this->getSkinName() ) . "';\n";
+ $s .= "var skin = '" . Xml::escapeJsString( $skinName ) . "';\n";
$s .= "var stylepath = '" . Xml::escapeJsString( $wgStylePath ) . "';";
$s .= "\n\n/* MediaWiki:Common.js */\n";
- $commonJs = wfMsgForContent('common.js');
- if ( !wfEmptyMsg ( 'common.js', $commonJs ) ) {
+ $commonJs = wfMsgExt( 'common.js', 'content' );
+ if ( !wfEmptyMsg( 'common.js', $commonJs ) ) {
$s .= $commonJs;
}
- $s .= "\n\n/* MediaWiki:".ucfirst( $this->getSkinName() ).".js */\n";
+ $s .= "\n\n/* MediaWiki:" . ucfirst( $skinName ) . ".js */\n";
// avoid inclusion of non defined user JavaScript (with custom skins only)
// by checking for default message content
- $msgKey = ucfirst( $this->getSkinName() ).'.js';
- $userJS = wfMsgForContent($msgKey);
+ $msgKey = ucfirst( $skinName ) . '.js';
+ $userJS = wfMsgExt( $msgKey, 'content' );
if ( !wfEmptyMsg( $msgKey, $userJS ) ) {
$s .= $userJS;
}
@@ -504,7 +536,7 @@ class Skin extends Linker {
}
/**
- * generate user stylesheet for action=raw&gen=css
+ * Generate user stylesheet for action=raw&gen=css
*/
public function generateUserStylesheet() {
wfProfileIn( __METHOD__ );
@@ -513,21 +545,21 @@ class Skin extends Linker {
wfProfileOut( __METHOD__ );
return $s;
}
-
+
/**
* Split for easier subclassing in SkinSimple, SkinStandard and SkinCologneBlue
*/
- protected function reallyGenerateUserStylesheet(){
+ protected function reallyGenerateUserStylesheet() {
global $wgUser;
$s = '';
- if (($undopt = $wgUser->getOption("underline")) < 2) {
+ if( ( $undopt = $wgUser->getOption( 'underline' ) ) < 2 ) {
$underline = $undopt ? 'underline' : 'none';
$s .= "a { text-decoration: $underline; }\n";
}
if( $wgUser->getOption( 'highlightbroken' ) ) {
$s .= "a.new, #quickbar a.new { color: #CC2200; }\n";
} else {
- $s .= <<<END
+ $s .= <<<CSS
a.new, #quickbar a.new,
a.stub, #quickbar a.stub {
color: inherit;
@@ -540,7 +572,7 @@ a.stub:after, #quickbar a.stub:after {
content: "!";
color: #772233;
}
-END;
+CSS;
}
if( $wgUser->getOption( 'justify' ) ) {
$s .= "#article, #bodyContent, #mw_content { text-align: justify; }\n";
@@ -551,6 +583,10 @@ END;
if( !$wgUser->getOption( 'editsection' ) ) {
$s .= ".editsection { display: none; }\n";
}
+ $fontstyle = $wgUser->getOption( 'editfont' );
+ if ( $fontstyle !== 'default' ) {
+ $s .= "textarea { font-family: $fontstyle; }\n";
+ }
return $s;
}
@@ -571,8 +607,8 @@ END;
);
// Add any extension CSS
- foreach( $out->getExtStyle() as $tag ) {
- $out->addStyle( $tag['href'] );
+ foreach ( $out->getExtStyle() as $url ) {
+ $out->addStyle( $url );
}
// If we use the site's dynamic CSS, throw that in, too
@@ -608,15 +644,16 @@ END;
// Per-user custom style pages
if( $wgAllowUserCss && $wgUser->isLoggedIn() ) {
- $action = $wgRequest->getVal('action');
+ $action = $wgRequest->getVal( 'action' );
# If we're previewing the CSS page, use it
if( $this->mTitle->isCssSubpage() && $this->userCanPreview( $action ) ) {
- $previewCss = $wgRequest->getText('wpTextbox1');
// @FIXME: properly escape the cdata!
- $this->usercss = "/*<![CDATA[*/\n" . $previewCss . "/*]]>*/";
+ $out->addInlineStyle( $wgRequest->getText( 'wpTextbox1' ) );
} else {
- $out->addStyle( self::makeUrl($this->userpage . '/' . $this->getSkinName() .'.css',
- 'action=raw&ctype=text/css' ) );
+ $out->addStyle( self::makeUrl(
+ $this->userpage . '/' . $this->getSkinName() . '.css',
+ 'action=raw&ctype=text/css' )
+ );
}
}
@@ -634,41 +671,16 @@ END;
$out->addStyle( 'common/common_rtl.css', '', '', 'rtl' );
}
- function getBodyOptions() {
- global $wgUser, $wgTitle, $wgOut, $wgRequest, $wgContLang;
-
- extract( $wgRequest->getValues( 'oldid', 'redirect', 'diff' ) );
-
- if ( 0 != $wgTitle->getNamespace() ) {
- $a = array( 'bgcolor' => '#ffffec' );
- }
- else $a = array( 'bgcolor' => '#FFFFFF' );
- if($wgOut->isArticle() && $wgUser->getOption('editondblclick') &&
- $wgTitle->quickUserCan( 'edit' ) ) {
- $s = $wgTitle->getFullURL( $this->editUrlOptions() );
- $s = 'document.location = "' .Xml::escapeJsString( $s ) .'";';
- $a += array ('ondblclick' => $s);
-
- }
- $a['onload'] = $wgOut->getOnloadHandler();
- $a['class'] =
- 'mediawiki' .
- ' '.( $wgContLang->isRTL() ? "rtl" : "ltr" ).
- ' '.$this->getPageClasses( $wgTitle ) .
- ' skin-'. Sanitizer::escapeClass( $this->getSkinName( ) );
- return $a;
- }
-
function getPageClasses( $title ) {
- $numeric = 'ns-'.$title->getNamespace();
+ $numeric = 'ns-' . $title->getNamespace();
if( $title->getNamespace() == NS_SPECIAL ) {
- $type = "ns-special";
+ $type = 'ns-special';
} elseif( $title->isTalkPage() ) {
- $type = "ns-talk";
+ $type = 'ns-talk';
} else {
- $type = "ns-subject";
+ $type = 'ns-subject';
}
- $name = Sanitizer::escapeClass( 'page-'.$title->getPrefixedText() );
+ $name = Sanitizer::escapeClass( 'page-' . $title->getPrefixedText() );
return "$numeric $type $name";
}
@@ -690,13 +702,13 @@ END;
function doBeforeContent() {
global $wgContLang;
- $fname = 'Skin::doBeforeContent';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$s = '';
$qb = $this->qbSetting();
- if( $langlinks = $this->otherLanguages() ) {
+ $langlinks = $this->otherLanguages();
+ if( $langlinks ) {
$rows = 2;
$borderhack = '';
} else {
@@ -710,24 +722,26 @@ END;
$shove = ( $qb != 0 );
$left = ( $qb == 1 || $qb == 3 );
- if( $wgContLang->isRTL() ) $left = !$left;
+ if( $wgContLang->isRTL() ) {
+ $left = !$left;
+ }
if( !$shove ) {
$s .= "<td class='top' align='left' valign='top' rowspan='{$rows}'>\n" .
- $this->logoText() . '</td>';
+ $this->logoText() . '</td>';
} elseif( $left ) {
$s .= $this->getQuickbarCompensator( $rows );
}
- $l = $wgContLang->isRTL() ? 'right' : 'left';
+ $l = $wgContLang->alignStart();
$s .= "<td {$borderhack} align='$l' valign='top'>\n";
- $s .= $this->topLinks() ;
- $s .= "<p class='subtitle'>" . $this->pageTitleLinks() . "</p>\n";
+ $s .= $this->topLinks();
+ $s .= '<p class="subtitle">' . $this->pageTitleLinks() . "</p>\n";
- $r = $wgContLang->isRTL() ? "left" : "right";
+ $r = $wgContLang->alignEnd();
$s .= "</td>\n<td {$borderhack} valign='top' align='$r' nowrap='nowrap'>";
$s .= $this->nameAndLogin();
- $s .= "\n<br />" . $this->searchForm() . "</td>";
+ $s .= "\n<br />" . $this->searchForm() . '</td>';
if ( $langlinks ) {
$s .= "</tr>\n<tr>\n<td class='top' colspan=\"2\">$langlinks</td>\n";
@@ -744,25 +758,26 @@ END;
$s .= "\n<div id='siteNotice'>$notice</div>\n";
}
$s .= $this->pageTitle();
- $s .= $this->pageSubtitle() ;
+ $s .= $this->pageSubtitle();
$s .= $this->getCategories();
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $s;
}
-
function getCategoryLinks() {
- global $wgOut, $wgTitle, $wgUseCategoryBrowser;
+ global $wgOut, $wgUseCategoryBrowser;
global $wgContLang, $wgUser;
- if( count( $wgOut->mCategoryLinks ) == 0 ) return '';
+ if( count( $wgOut->mCategoryLinks ) == 0 ) {
+ return '';
+ }
# Separator
$sep = wfMsgExt( 'catseparator', array( 'parsemag', 'escapenoentities' ) );
// Use Unicode bidi embedding override characters,
// to make sure links don't smash each other up in ugly ways.
- $dir = $wgContLang->isRTL() ? 'rtl' : 'ltr';
+ $dir = $wgContLang->getDir();
$embed = "<span dir='$dir'>";
$pop = '</span>';
@@ -770,11 +785,11 @@ END;
$s = '';
$colon = wfMsgExt( 'colon-separator', 'escapenoentities' );
if ( !empty( $allCats['normal'] ) ) {
- $t = $embed . implode ( "{$pop} {$sep} {$embed}" , $allCats['normal'] ) . $pop;
+ $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->link( Title::newFromText( wfMsgForContent('pagecategorieslink') ), $msg )
+ $this->link( Title::newFromText( wfMsgForContent( 'pagecategorieslink' ) ), $msg )
. $colon . $t . '</div>';
}
@@ -782,7 +797,7 @@ END;
if ( isset( $allCats['hidden'] ) ) {
if ( $wgUser->getBoolOption( 'showhiddencats' ) ) {
$class ='mw-hidden-cats-user-shown';
- } elseif ( $wgTitle->getNamespace() == NS_CATEGORY ) {
+ } elseif ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
$class = 'mw-hidden-cats-ns-shown';
} else {
$class = 'mw-hidden-cats-hidden';
@@ -790,64 +805,68 @@ END;
$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>";
+ '</div>';
}
# optional 'dmoz-like' category browser. Will be shown under the list
# of categories an article belong to
- if( $wgUseCategoryBrowser ){
+ if( $wgUseCategoryBrowser ) {
$s .= '<br /><hr />';
# get a big array of the parents tree
- $parenttree = $wgTitle->getParentCategoryTree();
+ $parenttree = $this->mTitle->getParentCategoryTree();
# Skin object passed by reference cause it can not be
# accessed under the method subfunction drawCategoryBrowser
- $tempout = explode("\n", Skin::drawCategoryBrowser($parenttree, $this) );
+ $tempout = explode( "\n", Skin::drawCategoryBrowser( $parenttree, $this ) );
# Clean out bogus first entry and sort them
- unset($tempout[0]);
- asort($tempout);
+ unset( $tempout[0] );
+ asort( $tempout );
# Output one per line
- $s .= implode("<br />\n", $tempout);
+ $s .= implode( "<br />\n", $tempout );
}
return $s;
}
- /** Render the array as a serie of links.
+ /**
+ * Render the array as a serie of links.
* @param $tree Array: categories tree returned by Title::getParentCategoryTree
* @param &skin Object: skin passed by reference
* @return String separated by &gt;, terminate with "\n"
*/
- function drawCategoryBrowser( $tree, &$skin ){
+ function drawCategoryBrowser( $tree, &$skin ) {
$return = '';
- foreach ($tree as $element => $parent) {
- if (empty($parent)) {
+ foreach( $tree as $element => $parent ) {
+ if( empty( $parent ) ) {
# element start a new list
$return .= "\n";
} else {
# grab the others elements
- $return .= Skin::drawCategoryBrowser($parent, $skin) . ' &gt; ';
+ $return .= Skin::drawCategoryBrowser( $parent, $skin ) . ' &gt; ';
}
# add our current element to the list
- $eltitle = Title::newFromText($element);
- $return .= $skin->link( $eltitle, $eltitle->getText() ) ;
+ $eltitle = Title::newFromText( $element );
+ $return .= $skin->link( $eltitle, $eltitle->getText() );
}
return $return;
}
function getCategories() {
- $catlinks=$this->getCategoryLinks();
+ $catlinks = $this->getCategoryLinks();
$classes = 'catlinks';
- if( strpos( $catlinks, '<div id="mw-normal-catlinks">' ) === false &&
- strpos( $catlinks, '<div id="mw-hidden-catlinks" class="mw-hidden-cats-hidden">' ) !== false ) {
+ // Check what we're showing
+ global $wgOut, $wgUser;
+ $allCats = $wgOut->getCategoryLinks();
+ $showHidden = $wgUser->getBoolOption( 'showhiddencats' ) ||
+ $this->mTitle->getNamespace() == NS_CATEGORY;
+
+ if( empty( $allCats['normal'] ) && !( !empty( $allCats['hidden'] ) && $showHidden ) ) {
$classes .= ' catlinks-allhidden';
}
- if( !empty( $catlinks ) ){
- return "<div id='catlinks' class='$classes'>{$catlinks}</div>";
- }
+ return "<div id='catlinks' class='$classes'>{$catlinks}</div>";
}
function getQuickbarCompensator( $rows = 1 ) {
@@ -869,12 +888,12 @@ END;
* Returns an empty string by default, if not changed by any hook function.
*/
protected function afterContentHook() {
- $data = "";
+ $data = '';
- if( wfRunHooks( 'SkinAfterContent', array( &$data ) ) ){
+ if( wfRunHooks( 'SkinAfterContent', array( &$data ) ) ) {
// adding just some spaces shouldn't toggle the output
// of the whole <div/>, so we use trim() here
- if( trim( $data ) != '' ){
+ if( trim( $data ) != '' ) {
// Doing this here instead of in the skins to
// ensure that the div has the same ID in all
// skins
@@ -897,13 +916,50 @@ END;
protected function generateDebugHTML() {
global $wgShowDebug, $wgOut;
if ( $wgShowDebug ) {
- $listInternals = str_replace( "\n", "</li>\n<li>", htmlspecialchars( $wgOut->mDebugtext ) );
- return "\n<hr>\n<strong>Debug data:</strong><ul style=\"font-family:monospace;\"><li>" .
- $listInternals . "</li></ul>\n";
+ $listInternals = $this->formatDebugHTML( $wgOut->mDebugtext );
+ return "\n<hr />\n<strong>Debug data:</strong><ul style=\"font-family:monospace;\" id=\"mw-debug-html\">" .
+ $listInternals . "</ul>\n";
}
return '';
}
+ private function formatDebugHTML( $debugText ) {
+ $lines = explode( "\n", $debugText );
+ $curIdent = 0;
+ $ret = '<li>';
+ foreach( $lines as $line ) {
+ $m = array();
+ $display = ltrim( $line );
+ $ident = strlen( $line ) - strlen( $display );
+ $diff = $ident - $curIdent;
+
+ if ( $display == '' ) {
+ $display = "\xc2\xa0";
+ }
+
+ if ( !$ident && $diff < 0 && substr( $display, 0, 9 ) != 'Entering ' && substr( $display, 0, 8 ) != 'Exiting ' ) {
+ $ident = $curIdent;
+ $diff = 0;
+ $display = '<span style="background:yellow;">' . htmlspecialchars( $display ) . '</span>';
+ } else {
+ $display = htmlspecialchars( $display );
+ }
+
+ if ( $diff < 0 ) {
+ $ret .= str_repeat( "</li></ul>\n", -$diff ) . "</li><li>\n";
+ } elseif ( $diff == 0 ) {
+ $ret .= "</li><li>\n";
+ } else {
+ $ret .= str_repeat( "<ul><li>\n", $diff );
+ }
+ $ret .= $display . "\n";
+
+ $curIdent = $ident;
+ }
+ $ret .= str_repeat( '</li></ul>', $curIdent ) . '</li>';
+ return $ret;
+ }
+
/**
* This gets called shortly before the </body> tag.
* @return String HTML to be put before </body>
@@ -918,17 +974,15 @@ END;
* @return String HTML-wrapped JS code to be put before </body>
*/
function bottomScripts() {
- global $wgJsMimeType;
- $bottomScriptText = "\n\t\t<script type=\"$wgJsMimeType\">if (window.runOnloadHook) runOnloadHook();</script>\n";
+ $bottomScriptText = "\n" . Html::inlineScript( 'if (window.runOnloadHook) runOnloadHook();' ) . "\n";
wfRunHooks( 'SkinAfterBottomScripts', array( $this, &$bottomScriptText ) );
return $bottomScriptText;
}
/** @return string Retrievied from HTML text */
function printSource() {
- global $wgTitle;
- $url = htmlspecialchars( $wgTitle->getFullURL() );
- return wfMsg( 'retrievedfrom', '<a href="'.$url.'">'.$url.'</a>' );
+ $url = htmlspecialchars( $this->mTitle->getFullURL() );
+ return wfMsg( 'retrievedfrom', '<a href="' . $url . '">' . $url . '</a>' );
}
function printFooter() {
@@ -937,10 +991,12 @@ END;
}
/** overloaded by derived classes */
- function doAfterContent() { return "</div></div>"; }
+ function doAfterContent() {
+ return '</div></div>';
+ }
function pageTitleLinks() {
- global $wgOut, $wgTitle, $wgUser, $wgRequest, $wgLang;
+ global $wgOut, $wgUser, $wgRequest, $wgLang;
$oldid = $wgRequest->getVal( 'oldid' );
$diff = $wgRequest->getVal( 'diff' );
@@ -957,9 +1013,9 @@ END;
}
if ( $wgOut->isArticleRelated() ) {
- if ( $wgTitle->getNamespace() == NS_FILE ) {
- $name = $wgTitle->getDBkey();
- $image = wfFindFile( $wgTitle );
+ if ( $this->mTitle->getNamespace() == NS_FILE ) {
+ $name = $this->mTitle->getDBkey();
+ $image = wfFindFile( $this->mTitle );
if( $image ) {
$link = htmlspecialchars( $image->getURL() );
$style = $this->getInternalLinkAttributes( $link, $name );
@@ -968,20 +1024,38 @@ END;
}
}
if ( 'history' == $action || isset( $diff ) || isset( $oldid ) ) {
- $s[] .= $this->makeKnownLinkObj( $wgTitle,
- wfMsg( 'currentrev' ) );
+ $s[] .= $this->link(
+ $this->mTitle,
+ wfMsg( 'currentrev' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
if ( $wgUser->getNewtalk() ) {
# do not show "You have new messages" text when we are viewing our
# own talk page
- if( !$wgTitle->equals( $wgUser->getTalkPage() ) ) {
- $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), wfMsgHtml( 'newmessageslink' ), 'redirect=no' );
- $dl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), wfMsgHtml( 'newmessagesdifflink' ), 'diff=cur' );
+ if( !$this->mTitle->equals( $wgUser->getTalkPage() ) ) {
+ $tl = $this->link(
+ $wgUser->getTalkPage(),
+ wfMsgHtml( 'newmessageslink' ),
+ array(),
+ array( 'redirect' => 'no' ),
+ array( 'known', 'noclasses' )
+ );
+
+ $dl = $this->link(
+ $wgUser->getTalkPage(),
+ wfMsgHtml( 'newmessagesdifflink' ),
+ array(),
+ array( 'diff' => 'cur' ),
+ array( 'known', 'noclasses' )
+ );
$s[] = '<strong>'. wfMsg( 'youhavenewmessages', $tl, $dl ) . '</strong>';
# disable caching
- $wgOut->setSquidMaxage(0);
- $wgOut->enableClientCache(false);
+ $wgOut->setSquidMaxage( 0 );
+ $wgOut->enableClientCache( false );
}
}
@@ -993,20 +1067,30 @@ END;
}
function getUndeleteLink() {
- global $wgUser, $wgTitle, $wgContLang, $wgLang, $action;
- if( $wgUser->isAllowed( 'deletedhistory' ) &&
- (($wgTitle->getArticleId() == 0) || ($action == "history")) &&
- ($n = $wgTitle->isDeleted() ) )
- {
- if ( $wgUser->isAllowed( 'undelete' ) ) {
- $msg = 'thisisdeleted';
- } else {
- $msg = 'viewdeleted';
+ global $wgUser, $wgContLang, $wgLang, $wgRequest;
+
+ $action = $wgRequest->getVal( 'action', 'view' );
+
+ if ( $wgUser->isAllowed( 'deletedhistory' ) &&
+ ( $this->mTitle->getArticleId() == 0 || $action == 'history' ) ) {
+ $n = $this->mTitle->isDeleted();
+ if ( $n ) {
+ if ( $wgUser->isAllowed( 'undelete' ) ) {
+ $msg = 'thisisdeleted';
+ } else {
+ $msg = 'viewdeleted';
+ }
+ return wfMsg(
+ $msg,
+ $this->link(
+ SpecialPage::getTitleFor( 'Undelete', $this->mTitle->getPrefixedDBkey() ),
+ wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ )
+ );
}
- return wfMsg( $msg,
- $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Undelete', $wgTitle->getPrefixedDBkey() ),
- wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $wgLang->formatNum( $n ) ) ) );
}
return '';
}
@@ -1014,9 +1098,13 @@ END;
function printableLink() {
global $wgOut, $wgFeedClasses, $wgRequest, $wgLang;
- $printurl = $wgRequest->escapeAppendQuery( 'printable=yes' );
+ $s = array();
+
+ if ( !$wgOut->isPrintable() ) {
+ $printurl = $wgRequest->escapeAppendQuery( 'printable=yes' );
+ $s[] = "<a href=\"$printurl\" rel=\"alternate\">" . wfMsg( 'printableversion' ) . '</a>';
+ }
- $s[] = "<a href=\"$printurl\" rel=\"alternate\">" . wfMsg( 'printableversion' ) . '</a>';
if( $wgOut->isSyndicated() ) {
foreach( $wgFeedClasses as $format => $class ) {
$feedurl = $wgRequest->escapeAppendQuery( "feed=$format" );
@@ -1027,6 +1115,10 @@ END;
return $wgLang->pipeList( $s );
}
+ /**
+ * Gets the h1 element with the page title.
+ * @return string
+ */
function pageTitle() {
global $wgOut;
$s = '<h1 class="pagetitle">' . $wgOut->getPageTitle() . '</h1>';
@@ -1037,39 +1129,46 @@ END;
global $wgOut;
$sub = $wgOut->getSubtitle();
- if ( '' == $sub ) {
+ if ( $sub == '' ) {
global $wgExtraSubtitle;
$sub = wfMsgExt( 'tagline', 'parsemag' ) . $wgExtraSubtitle;
}
$subpages = $this->subPageSubtitle();
- $sub .= !empty($subpages)?"</p><p class='subpages'>$subpages":'';
+ $sub .= !empty( $subpages ) ? "</p><p class='subpages'>$subpages" : '';
$s = "<p class='subtitle'>{$sub}</p>\n";
return $s;
}
function subPageSubtitle() {
$subpages = '';
- if(!wfRunHooks('SkinSubPageSubtitle', array(&$subpages)))
+ if( !wfRunHooks( 'SkinSubPageSubtitle', array( &$subpages ) ) ) {
return $subpages;
+ }
- global $wgOut, $wgTitle;
- if($wgOut->isArticle() && MWNamespace::hasSubpages( $wgTitle->getNamespace() )) {
- $ptext=$wgTitle->getPrefixedText();
- if(preg_match('/\//',$ptext)) {
- $links = explode('/',$ptext);
+ global $wgOut;
+ if( $wgOut->isArticle() && MWNamespace::hasSubpages( $this->mTitle->getNamespace() ) ) {
+ $ptext = $this->mTitle->getPrefixedText();
+ if( preg_match( '/\//', $ptext ) ) {
+ $links = explode( '/', $ptext );
array_pop( $links );
$c = 0;
$growinglink = '';
$display = '';
- foreach($links as $link) {
+ foreach( $links as $link ) {
$growinglink .= $link;
$display .= $link;
$linkObj = Title::newFromText( $growinglink );
- if( is_object( $linkObj ) && $linkObj->exists() ){
- $getlink = $this->makeKnownLinkObj( $linkObj, htmlspecialchars( $display ) );
+ if( is_object( $linkObj ) && $linkObj->exists() ) {
+ $getlink = $this->link(
+ $linkObj,
+ htmlspecialchars( $display ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
$c++;
- if ($c>1) {
- $subpages .= wfMsgExt( 'pipe-separator' , 'escapenoentities' );
+ if( $c > 1 ) {
+ $subpages .= wfMsgExt( 'pipe-separator', 'escapenoentities' );
} else {
$subpages .= '&lt; ';
}
@@ -1094,7 +1193,7 @@ END;
}
function nameAndLogin() {
- global $wgUser, $wgTitle, $wgLang, $wgContLang;
+ global $wgUser, $wgLang, $wgContLang;
$logoutPage = $wgContLang->specialPage( 'Userlogout' );
@@ -1111,7 +1210,7 @@ END;
$ret .= wfMsg( 'notloggedin' );
}
- $returnTo = $wgTitle->getPrefixedDBkey();
+ $returnTo = $this->mTitle->getPrefixedDBkey();
$query = array();
if ( $logoutPage != $returnTo ) {
$query['returnto'] = $returnTo;
@@ -1125,7 +1224,7 @@ END;
wfMsg( $loginlink ), array(), $query
);
} else {
- $returnTo = $wgTitle->getPrefixedDBkey();
+ $returnTo = $this->mTitle->getPrefixedDBkey();
$talkLink = $this->link( $wgUser->getTalkPage(),
$wgLang->getNsText( NS_TALK ) );
@@ -1164,22 +1263,23 @@ END;
global $wgRequest, $wgUseTwoButtonsSearchForm;
$search = $wgRequest->getText( 'search' );
- $s = '<form id="searchform'.$this->searchboxes.'" 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" id="searchInput'.$this->searchboxes.'" name="search" size="19" value="'
- . htmlspecialchars(substr($search,0,256)) . "\" />\n"
- . '<input type="submit" name="go" value="' . wfMsg ('searcharticle') . '" />';
-
- if ($wgUseTwoButtonsSearchForm)
- $s .= '&nbsp;<input type="submit" name="fulltext" value="' . wfMsg ('searchbutton') . "\" />\n";
- else
- $s .= ' <a href="' . $this->escapeSearchLink() . '" rel="search">' . wfMsg ('powersearch-legend') . "</a>\n";
-
+ . '<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' ) . '" />';
+
+ if( $wgUseTwoButtonsSearchForm ) {
+ $s .= '&nbsp;<input type="submit" name="fulltext" value="' . wfMsg( 'searchbutton' ) . "\" />\n";
+ } else {
+ $s .= ' <a href="' . $this->escapeSearchLink() . '" rel="search">' . wfMsg( 'powersearch-legend' ) . "</a>\n";
+ }
+
$s .= '</form>';
-
+
// Ensure unique id's for search boxes made after the first
$this->searchboxes = $this->searchboxes == '' ? 2 : $this->searchboxes + 1;
-
+
return $s;
}
@@ -1207,7 +1307,7 @@ END;
}
// FIXME: Is using Language::pipeList impossible here? Do not quite understand the use of the newline
- return implode( $s, wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "\n" );
+ return implode( $s, wfMsgExt( 'pipe-separator', 'escapenoentities' ) . "\n" );
}
/**
@@ -1244,22 +1344,26 @@ END;
function variantLinks() {
$s = '';
/* show links to different language variants */
- global $wgDisableLangConversion, $wgLang, $wgContLang, $wgTitle;
+ global $wgDisableLangConversion, $wgLang, $wgContLang;
$variants = $wgContLang->getVariants();
if( !$wgDisableLangConversion && sizeof( $variants ) > 1 ) {
foreach( $variants as $code ) {
$varname = $wgContLang->getVariantname( $code );
- if( $varname == 'disable' )
+ if( $varname == 'disable' ) {
continue;
- $s = $wgLang->pipeList( array( $s, '<a href="' . $wgTitle->escapeLocalUrl( 'variant=' . $code ) . '">' . htmlspecialchars( $varname ) . '</a>' ) );
+ }
+ $s = $wgLang->pipeList( array(
+ $s,
+ '<a href="' . $this->mTitle->escapeLocalURL( 'variant=' . $code ) . '">' . htmlspecialchars( $varname ) . '</a>'
+ ) );
}
}
return $s;
}
function bottomLinks() {
- global $wgOut, $wgUser, $wgTitle, $wgUseTrackbacks;
- $sep = wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "\n";
+ global $wgOut, $wgUser, $wgUseTrackbacks;
+ $sep = wfMsgExt( 'pipe-separator', 'escapenoentities' ) . "\n";
$s = '';
if ( $wgOut->isArticleRelated() ) {
@@ -1272,31 +1376,41 @@ END;
$element[] = $this->whatLinksHere();
$element[] = $this->watchPageLinksLink();
- if ($wgUseTrackbacks)
+ if( $wgUseTrackbacks ) {
$element[] = $this->trackbackLink();
+ }
- if ( $wgTitle->getNamespace() == NS_USER
- || $wgTitle->getNamespace() == NS_USER_TALK )
-
+ if (
+ $this->mTitle->getNamespace() == NS_USER ||
+ $this->mTitle->getNamespace() == NS_USER_TALK
+ )
{
- $id=User::idFromName($wgTitle->getText());
- $ip=User::isIP($wgTitle->getText());
+ $id = User::idFromName( $this->mTitle->getText() );
+ $ip = User::isIP( $this->mTitle->getText() );
- if($id || $ip) { # both anons and non-anons have contri list
+ # Both anons and non-anons have contributions list
+ if( $id || $ip ) {
$element[] = $this->userContribsLink();
}
if( $this->showEmailUser( $id ) ) {
$element[] = $this->emailUserLink();
}
}
-
+
$s = implode( $element, $sep );
- if ( $wgTitle->getArticleId() ) {
+ if ( $this->mTitle->getArticleId() ) {
$s .= "\n<br />";
- if($wgUser->isAllowed('delete')) { $s .= $this->deleteThisPage(); }
- if($wgUser->isAllowed('protect')) { $s .= $sep . $this->protectThisPage(); }
- if($wgUser->isAllowed('move')) { $s .= $sep . $this->moveThisPage(); }
+ // Delete/protect/move links for privileged users
+ if( $wgUser->isAllowed( 'delete' ) ) {
+ $s .= $this->deleteThisPage();
+ }
+ if( $wgUser->isAllowed( 'protect' ) ) {
+ $s .= $sep . $this->protectThisPage();
+ }
+ if( $wgUser->isAllowed( 'move' ) ) {
+ $s .= $sep . $this->moveThisPage();
+ }
}
$s .= "<br />\n" . $this->otherLanguages();
}
@@ -1306,14 +1420,22 @@ END;
function pageStats() {
global $wgOut, $wgLang, $wgArticle, $wgRequest, $wgUser;
- global $wgDisableCounters, $wgMaxCredits, $wgShowCreditsIfMax, $wgTitle, $wgPageShowWatchingUsers;
+ global $wgDisableCounters, $wgMaxCredits, $wgShowCreditsIfMax, $wgPageShowWatchingUsers;
$oldid = $wgRequest->getVal( 'oldid' );
$diff = $wgRequest->getVal( 'diff' );
- if ( ! $wgOut->isArticle() ) { return ''; }
- if( !$wgArticle instanceOf Article ) { return ''; }
- if ( isset( $oldid ) || isset( $diff ) ) { return ''; }
- if ( 0 == $wgArticle->getID() ) { return ''; }
+ if ( !$wgOut->isArticle() ) {
+ return '';
+ }
+ if( !$wgArticle instanceof Article ) {
+ return '';
+ }
+ if ( isset( $oldid ) || isset( $diff ) ) {
+ return '';
+ }
+ if ( 0 == $wgArticle->getID() ) {
+ return '';
+ }
$s = '';
if ( !$wgDisableCounters ) {
@@ -1323,7 +1445,7 @@ END;
}
}
- if( $wgMaxCredits != 0 ){
+ if( $wgMaxCredits != 0 ) {
$s .= ' ' . Credits::getCredits( $wgArticle, $wgMaxCredits, $wgShowCreditsIfMax );
} else {
$s .= $this->lastModified();
@@ -1331,15 +1453,19 @@ END;
if( $wgPageShowWatchingUsers && $wgUser->getOption( 'shownumberswatching' ) ) {
$dbr = wfGetDB( DB_SLAVE );
- $watchlist = $dbr->tableName( 'watchlist' );
- $sql = "SELECT COUNT(*) AS n FROM $watchlist
- WHERE wl_title='" . $dbr->strencode($wgTitle->getDBkey()) .
- "' AND wl_namespace=" . $wgTitle->getNamespace() ;
- $res = $dbr->query( $sql, 'Skin::pageStats');
+ $res = $dbr->select(
+ 'watchlist',
+ array( 'COUNT(*) AS n' ),
+ array(
+ 'wl_title' => $dbr->strencode( $this->mTitle->getDBkey() ),
+ 'wl_namespace' => $this->mTitle->getNamespace()
+ ),
+ __METHOD__
+ );
$x = $dbr->fetchObject( $res );
$s .= ' ' . wfMsgExt( 'number_of_watching_users_pageview',
- array( 'parseinline' ), $wgLang->formatNum($x->n)
+ array( 'parseinline' ), $wgLang->formatNum( $x->n )
);
}
@@ -1367,7 +1493,8 @@ END;
$out = '';
if( $wgRightsPage ) {
- $link = $this->makeKnownLink( $wgRightsPage, $wgRightsText );
+ $title = Title::newFromText( $wgRightsPage );
+ $link = $this->linkKnown( $title, $wgRightsText );
} elseif( $wgRightsUrl ) {
$link = $this->makeExternalLink( $wgRightsUrl, $wgRightsText );
} elseif( $wgRightsText ) {
@@ -1376,6 +1503,11 @@ END;
# Give up now
return $out;
}
+ // Allow for site and per-namespace customization of copyright notice.
+ if( isset( $wgArticle ) ) {
+ wfRunHooks( 'SkinCopyrightFooter', array( $wgArticle->getTitle(), $type, &$msg, &$link ) );
+ }
+
$out .= wfMsgForContent( $msg, $link );
return $out;
}
@@ -1385,14 +1517,14 @@ END;
$out = '';
if ( isset( $wgCopyrightIcon ) && $wgCopyrightIcon ) {
$out = $wgCopyrightIcon;
- } else if ( $wgRightsIcon ) {
+ } elseif ( $wgRightsIcon ) {
$icon = htmlspecialchars( $wgRightsIcon );
if ( $wgRightsUrl ) {
$url = htmlspecialchars( $wgRightsUrl );
$out .= '<a href="'.$url.'">';
}
$text = htmlspecialchars( $wgRightsText );
- $out .= "<img src=\"$icon\" alt='$text' />";
+ $out .= "<img src=\"$icon\" alt=\"$text\" width=\"88\" height=\"31\" />";
if ( $wgRightsUrl ) {
$out .= '</a>';
}
@@ -1400,16 +1532,20 @@ END;
return $out;
}
+ /**
+ * Gets the powered by MediaWiki icon.
+ * @return string
+ */
function getPoweredBy() {
global $wgStylePath;
$url = htmlspecialchars( "$wgStylePath/common/images/poweredby_mediawiki_88x31.png" );
- $img = '<a href="http://www.mediawiki.org/"><img src="'.$url.'" alt="Powered by MediaWiki" /></a>';
+ $img = '<a href="http://www.mediawiki.org/"><img src="' . $url . '" height="31" width="88" alt="Powered by MediaWiki" /></a>';
return $img;
}
function lastModified() {
global $wgLang, $wgArticle;
- if( $this->mRevisionId ) {
+ if( $this->mRevisionId && $this->mRevisionId != $wgArticle->getLatest() ) {
$timestamp = Revision::getTimestampFromId( $wgArticle->getTitle(), $this->mRevisionId );
} else {
$timestamp = $wgArticle->getTimestamp();
@@ -1428,12 +1564,15 @@ END;
}
function logoText( $align = '' ) {
- if ( '' != $align ) { $a = " align='{$align}'"; }
- else { $a = ''; }
+ if ( $align != '' ) {
+ $a = " align='{$align}'";
+ } else {
+ $a = '';
+ }
$mp = wfMsg( 'mainpage' );
$mptitle = Title::newMainPage();
- $url = ( is_object($mptitle) ? $mptitle->escapeLocalURL() : '' );
+ $url = ( is_object( $mptitle ) ? $mptitle->escapeLocalURL() : '' );
$logourl = $this->getLogo();
$s = "<a href='{$url}'><img{$a} src='{$logourl}' alt='[{$mp}]' /></a>";
@@ -1441,7 +1580,7 @@ END;
}
/**
- * show a drop-down box of special pages
+ * Show a drop-down box of special pages
*/
function specialPagesList() {
global $wgUser, $wgContLang, $wgServer, $wgRedirectScript;
@@ -1470,18 +1609,22 @@ END;
return $s;
}
+ /**
+ * Gets the link to the wiki's main page.
+ * @return string
+ */
function mainPageLink() {
- $s = $this->makeKnownLinkObj( Title::newMainPage(), wfMsg( 'mainpage' ) );
- return $s;
- }
-
- function copyrightLink() {
- $s = $this->makeKnownLink( wfMsgForContent( 'copyrightpage' ),
- wfMsg( 'copyrightpagename' ) );
+ $s = $this->link(
+ Title::newMainPage(),
+ wfMsg( 'mainpage' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
return $s;
}
- private function footerLink ( $desc, $page ) {
+ private function footerLink( $desc, $page ) {
// if the link description has been set to "-" in the default language,
if ( wfMsgForContent( $desc ) == '-') {
// then it is disabled, for all languages.
@@ -1490,38 +1633,56 @@ END;
// Otherwise, we display the link for the user, described in their
// language (which may or may not be the same as the default language),
// but we make the link target be the one site-wide page.
- return $this->makeKnownLink( wfMsgForContent( $page ),
- wfMsgExt( $desc, array( 'parsemag', 'escapenoentities' ) ) );
+ $title = Title::newFromText( wfMsgForContent( $page ) );
+ return $this->linkKnown(
+ $title,
+ wfMsgExt( $desc, array( 'parsemag', 'escapenoentities' ) )
+ );
}
}
+ /**
+ * Gets the link to the wiki's privacy policy page.
+ */
function privacyLink() {
return $this->footerLink( 'privacy', 'privacypage' );
}
+ /**
+ * Gets the link to the wiki's about page.
+ */
function aboutLink() {
return $this->footerLink( 'aboutsite', 'aboutpage' );
}
+ /**
+ * Gets the link to the wiki's general disclaimers page.
+ */
function disclaimerLink() {
return $this->footerLink( 'disclaimers', 'disclaimerpage' );
}
function editThisPage() {
- global $wgOut, $wgTitle;
+ global $wgOut;
if ( !$wgOut->isArticleRelated() ) {
$s = wfMsg( 'protectedpage' );
} else {
- if( $wgTitle->quickUserCan( 'edit' ) && $wgTitle->exists() ) {
+ if( $this->mTitle->quickUserCan( 'edit' ) && $this->mTitle->exists() ) {
$t = wfMsg( 'editthispage' );
- } elseif( $wgTitle->quickUserCan( 'create' ) && !$wgTitle->exists() ) {
+ } elseif( $this->mTitle->quickUserCan( 'create' ) && !$this->mTitle->exists() ) {
$t = wfMsg( 'create-this-page' );
} else {
$t = wfMsg( 'viewsource' );
}
- $s = $this->makeKnownLinkObj( $wgTitle, $t, $this->editUrlOptions() );
+ $s = $this->link(
+ $this->mTitle,
+ $t,
+ array(),
+ $this->editUrlOptions(),
+ array( 'known', 'noclasses' )
+ );
}
return $s;
}
@@ -1530,27 +1691,35 @@ END;
* Return URL options for the 'edit page' link.
* This may include an 'oldid' specifier, if the current page view is such.
*
- * @return string
+ * @return array
* @private
*/
function editUrlOptions() {
global $wgArticle;
+ $options = array( 'action' => 'edit' );
+
if( $this->mRevisionId && ! $wgArticle->isCurrent() ) {
- return "action=edit&oldid=" . intval( $this->mRevisionId );
- } else {
- return "action=edit";
+ $options['oldid'] = intval( $this->mRevisionId );
}
+
+ return $options;
}
function deleteThisPage() {
- global $wgUser, $wgTitle, $wgRequest;
+ global $wgUser, $wgRequest;
$diff = $wgRequest->getVal( 'diff' );
- if ( $wgTitle->getArticleId() && ( ! $diff ) && $wgUser->isAllowed('delete') ) {
+ if ( $this->mTitle->getArticleId() && ( !$diff ) && $wgUser->isAllowed( 'delete' ) ) {
$t = wfMsg( 'deletethispage' );
- $s = $this->makeKnownLinkObj( $wgTitle, $t, 'action=delete' );
+ $s = $this->link(
+ $this->mTitle,
+ $t,
+ array(),
+ array( 'action' => 'delete' ),
+ array( 'known', 'noclasses' )
+ );
} else {
$s = '';
}
@@ -1558,18 +1727,25 @@ END;
}
function protectThisPage() {
- global $wgUser, $wgTitle, $wgRequest;
+ global $wgUser, $wgRequest;
$diff = $wgRequest->getVal( 'diff' );
- if ( $wgTitle->getArticleId() && ( ! $diff ) && $wgUser->isAllowed('protect') ) {
- if ( $wgTitle->isProtected() ) {
- $t = wfMsg( 'unprotectthispage' );
- $q = 'action=unprotect';
+ if ( $this->mTitle->getArticleId() && ( ! $diff ) && $wgUser->isAllowed('protect') ) {
+ if ( $this->mTitle->isProtected() ) {
+ $text = wfMsg( 'unprotectthispage' );
+ $query = array( 'action' => 'unprotect' );
} else {
- $t = wfMsg( 'protectthispage' );
- $q = 'action=protect';
+ $text = wfMsg( 'protectthispage' );
+ $query = array( 'action' => 'protect' );
}
- $s = $this->makeKnownLinkObj( $wgTitle, $t, $q );
+
+ $s = $this->link(
+ $this->mTitle,
+ $text,
+ array(),
+ $query,
+ array( 'known', 'noclasses' )
+ );
} else {
$s = '';
}
@@ -1577,20 +1753,27 @@ END;
}
function watchThisPage() {
- global $wgOut, $wgTitle;
+ global $wgOut;
++$this->mWatchLinkNum;
if ( $wgOut->isArticleRelated() ) {
- if ( $wgTitle->userIsWatching() ) {
- $t = wfMsg( 'unwatchthispage' );
- $q = 'action=unwatch';
- $id = "mw-unwatch-link".$this->mWatchLinkNum;
+ if ( $this->mTitle->userIsWatching() ) {
+ $text = wfMsg( 'unwatchthispage' );
+ $query = array( 'action' => 'unwatch' );
+ $id = 'mw-unwatch-link' . $this->mWatchLinkNum;
} else {
- $t = wfMsg( 'watchthispage' );
- $q = 'action=watch';
- $id = 'mw-watch-link'.$this->mWatchLinkNum;
+ $text = wfMsg( 'watchthispage' );
+ $query = array( 'action' => 'watch' );
+ $id = 'mw-watch-link' . $this->mWatchLinkNum;
}
- $s = $this->makeKnownLinkObj( $wgTitle, $t, $q, '', '', " id=\"$id\"" );
+
+ $s = $this->link(
+ $this->mTitle,
+ $text,
+ array( 'id' => $id ),
+ $query,
+ array( 'known', 'noclasses' )
+ );
} else {
$s = wfMsg( 'notanarticle' );
}
@@ -1598,11 +1781,14 @@ END;
}
function moveThisPage() {
- global $wgTitle;
-
- if ( $wgTitle->quickUserCan( 'move' ) ) {
- return $this->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
- wfMsg( 'movethispage' ), 'target=' . $wgTitle->getPrefixedURL() );
+ if ( $this->mTitle->quickUserCan( 'move' ) ) {
+ return $this->link(
+ SpecialPage::getTitleFor( 'Movepage' ),
+ wfMsg( 'movethispage' ),
+ array(),
+ array( 'target' => $this->mTitle->getPrefixedDBkey() ),
+ array( 'known', 'noclasses' )
+ );
} else {
// no message if page is protected - would be redundant
return '';
@@ -1610,60 +1796,69 @@ END;
}
function historyLink() {
- global $wgTitle;
-
- return $this->link( $wgTitle, wfMsg( 'history' ),
- array( 'rel' => 'archives' ), array( 'action' => 'history' ) );
+ return $this->link(
+ $this->mTitle,
+ wfMsgHtml( 'history' ),
+ array( 'rel' => 'archives' ),
+ array( 'action' => 'history' )
+ );
}
function whatLinksHere() {
- global $wgTitle;
-
- return $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Whatlinkshere', $wgTitle->getPrefixedDBkey() ),
- wfMsg( 'whatlinkshere' ) );
+ return $this->link(
+ SpecialPage::getTitleFor( 'Whatlinkshere', $this->mTitle->getPrefixedDBkey() ),
+ wfMsgHtml( 'whatlinkshere' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
function userContribsLink() {
- global $wgTitle;
-
- return $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Contributions', $wgTitle->getDBkey() ),
- wfMsg( 'contributions' ) );
+ return $this->link(
+ SpecialPage::getTitleFor( 'Contributions', $this->mTitle->getDBkey() ),
+ wfMsgHtml( 'contributions' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
function showEmailUser( $id ) {
global $wgUser;
$targetUser = User::newFromId( $id );
- return $wgUser->canSendEmail() && # the sending user must have a confirmed email address
+ return $wgUser->canSendEmail() && # the sending user must have a confirmed email address
$targetUser->canReceiveEmail(); # the target user must have a confirmed email address and allow emails from users
}
function emailUserLink() {
- global $wgTitle;
-
- return $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Emailuser', $wgTitle->getDBkey() ),
- wfMsg( 'emailuser' ) );
+ return $this->link(
+ SpecialPage::getTitleFor( 'Emailuser', $this->mTitle->getDBkey() ),
+ wfMsg( 'emailuser' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
function watchPageLinksLink() {
- global $wgOut, $wgTitle;
-
- if ( ! $wgOut->isArticleRelated() ) {
+ global $wgOut;
+ if ( !$wgOut->isArticleRelated() ) {
return '(' . wfMsg( 'notanarticle' ) . ')';
} else {
- return $this->makeKnownLinkObj(
- SpecialPage::getTitleFor( 'Recentchangeslinked', $wgTitle->getPrefixedDBkey() ),
- wfMsg( 'recentchangeslinked' ) );
+ return $this->link(
+ SpecialPage::getTitleFor( 'Recentchangeslinked', $this->mTitle->getPrefixedDBkey() ),
+ wfMsg( 'recentchangeslinked-toolbox' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
}
function trackbackLink() {
- global $wgTitle;
-
- return "<a href=\"" . $wgTitle->trackbackURL() . "\">"
- . wfMsg('trackbacklink') . "</a>";
+ return '<a href="' . $this->mTitle->trackbackURL() . '">'
+ . wfMsg( 'trackbacklink' ) . '</a>';
}
function otherLanguages() {
@@ -1680,35 +1875,41 @@ END;
$s = wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' );
$first = true;
- if($wgContLang->isRTL()) $s .= '<span dir="LTR">';
+ if( $wgContLang->isRTL() ) {
+ $s .= '<span dir="LTR">';
+ }
foreach( $a as $l ) {
- if ( ! $first ) { $s .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); }
+ if ( !$first ) {
+ $s .= wfMsgExt( 'pipe-separator', 'escapenoentities' );
+ }
$first = false;
$nt = Title::newFromText( $l );
$url = $nt->escapeFullURL();
$text = $wgContLang->getLanguageName( $nt->getInterwiki() );
- if ( '' == $text ) { $text = $l; }
- $style = $this->getExternalLinkAttributes( $l, $text );
+ if ( $text == '' ) {
+ $text = $l;
+ }
+ $style = $this->getExternalLinkAttributes();
$s .= "<a href=\"{$url}\"{$style}>{$text}</a>";
}
- if($wgContLang->isRTL()) $s .= '</span>';
+ if( $wgContLang->isRTL() ) {
+ $s .= '</span>';
+ }
return $s;
}
function talkLink() {
- global $wgTitle;
-
- if ( NS_SPECIAL == $wgTitle->getNamespace() ) {
+ if ( NS_SPECIAL == $this->mTitle->getNamespace() ) {
# No discussion links for special pages
return '';
}
$linkOptions = array();
- if( $wgTitle->isTalkPage() ) {
- $link = $wgTitle->getSubjectPage();
+ if( $this->mTitle->isTalkPage() ) {
+ $link = $this->mTitle->getSubjectPage();
switch( $link->getNamespace() ) {
case NS_MAIN:
$text = wfMsg( 'articlepage' );
@@ -1741,7 +1942,7 @@ END;
$text = wfMsg( 'articlepage' );
}
} else {
- $link = $wgTitle->getTalkPage();
+ $link = $this->mTitle->getTalkPage();
$text = wfMsg( 'talkpage' );
}
@@ -1751,24 +1952,33 @@ END;
}
function commentLink() {
- global $wgTitle, $wgOut;
+ global $wgOut;
- if ( $wgTitle->getNamespace() == NS_SPECIAL ) {
+ if ( $this->mTitle->getNamespace() == NS_SPECIAL ) {
return '';
}
# __NEWSECTIONLINK___ changes behaviour here
- # If it's present, the link points to this page, otherwise
+ # If it is present, the link points to this page, otherwise
# it points to the talk page
- if( $wgTitle->isTalkPage() ) {
- $title = $wgTitle;
+ if( $this->mTitle->isTalkPage() ) {
+ $title = $this->mTitle;
} elseif( $wgOut->showNewSectionLink() ) {
- $title = $wgTitle;
+ $title = $this->mTitle;
} else {
- $title = $wgTitle->getTalkPage();
+ $title = $this->mTitle->getTalkPage();
}
- return $this->makeKnownLinkObj( $title, wfMsg( 'postcomment' ), 'action=edit&section=new' );
+ return $this->link(
+ $title,
+ wfMsg( 'postcomment' ),
+ array(),
+ array(
+ 'action' => 'edit',
+ 'section' => 'new'
+ ),
+ array( 'known', 'noclasses' )
+ );
}
/* these are used extensively in SkinTemplate, but also some other places */
@@ -1800,8 +2010,10 @@ END;
return $title->getLocalURL( $urlaction );
}
- # If url string starts with http, consider as external URL, else
- # internal
+ /**
+ * If url string starts with http, consider as external URL, else
+ * internal
+ */
static function makeInternalOrExternalUrl( $name ) {
if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $name ) ) {
return $name;
@@ -1870,27 +2082,49 @@ END;
}
$bar = array();
- $lines = explode( "\n", wfMsgForContent( 'sidebar' ) );
+ $this->addToSidebar( $bar, 'sidebar' );
+
+ wfRunHooks( 'SkinBuildSidebar', array( $this, &$bar ) );
+ if ( $wgEnableSidebarCache ) {
+ $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry );
+ }
+ wfProfileOut( __METHOD__ );
+ return $bar;
+ }
+ /**
+ * Add content from a sidebar system message
+ * Currently only used for MediaWiki:Sidebar (but may be used by Extensions)
+ *
+ * @param &$bar array
+ * @param $message String
+ */
+ function addToSidebar( &$bar, $message ) {
+ $lines = explode( "\n", wfMsgForContent( $message ) );
$heading = '';
- foreach ($lines as $line) {
- if (strpos($line, '*') !== 0)
+ foreach( $lines as $line ) {
+ if( strpos( $line, '*' ) !== 0 ) {
continue;
- if (strpos($line, '**') !== 0) {
- $line = trim($line, '* ');
- $heading = $line;
- if( !array_key_exists($heading, $bar) ) $bar[$heading] = array();
+ }
+ if( strpos( $line, '**') !== 0 ) {
+ $heading = trim( $line, '* ' );
+ if( !array_key_exists( $heading, $bar ) ) {
+ $bar[$heading] = array();
+ }
} else {
- if (strpos($line, '|') !== false) { // sanity check
- $line = array_map('trim', explode( '|' , trim($line, '* '), 2 ) );
+ if( strpos( $line, '|' ) !== false ) { // sanity check
+ $line = array_map( 'trim', explode( '|', trim( $line, '* ' ), 2 ) );
$link = wfMsgForContent( $line[0] );
- if ($link == '-')
+ if( $link == '-' ) {
continue;
+ }
- $text = wfMsgExt($line[1], 'parsemag');
- if (wfEmptyMsg($line[1], $text))
+ $text = wfMsgExt( $line[1], 'parsemag' );
+ if( wfEmptyMsg( $line[1], $text ) ) {
$text = $line[1];
- if (wfEmptyMsg($line[0], $link))
+ }
+ if( wfEmptyMsg( $line[0], $link ) ) {
$link = $line[0];
+ }
if ( preg_match( '/^(?:' . wfUrlProtocols() . ')/', $link ) ) {
$href = $link;
@@ -1907,15 +2141,25 @@ END;
$bar[$heading][] = array(
'text' => $text,
'href' => $href,
- 'id' => 'n-' . strtr($line[1], ' ', '-'),
+ 'id' => 'n-' . strtr( $line[1], ' ', '-' ),
'active' => false
);
- } else { continue; }
+ } else {
+ continue;
+ }
}
}
- wfRunHooks('SkinBuildSidebar', array($this, &$bar));
- if ( $wgEnableSidebarCache ) $parserMemc->set( $key, $bar, $wgSidebarCacheExpiry );
- wfProfileOut( __METHOD__ );
- return $bar;
+ }
+
+ /**
+ * Should we include common/wikiprintable.css? Skins that have their own
+ * print stylesheet should override this and return false. (This is an
+ * ugly hack to get Monobook to play nicely with
+ * OutputPage::headElement().)
+ *
+ * @return bool
+ */
+ public function commonPrintStylesheet() {
+ return true;
}
}
diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php
index 4317a93e..e5fdb274 100644
--- a/includes/SkinTemplate.php
+++ b/includes/SkinTemplate.php
@@ -27,11 +27,11 @@ if ( ! defined( 'MEDIAWIKI' ) )
class MediaWiki_I18N {
var $_context = array();
- function set($varName, $value) {
+ function set( $varName, $value ) {
$this->_context[$varName] = $value;
}
- function translate($value) {
+ function translate( $value ) {
wfProfileIn( __METHOD__ );
// Hack for i18n:attributes in PHPTAL 1.0.0 dev version as of 2004-10-23
@@ -40,12 +40,12 @@ class MediaWiki_I18N {
$value = wfMsg( $value );
// interpolate variables
$m = array();
- while (preg_match('/\$([0-9]*?)/sm', $value, $m)) {
- list($src, $var) = $m;
+ while( preg_match( '/\$([0-9]*?)/sm', $value, $m ) ) {
+ list( $src, $var ) = $m;
wfSuppressWarnings();
$varValue = $this->_context[$var];
wfRestoreWarnings();
- $value = str_replace($src, $varValue, $value);
+ $value = str_replace( $src, $varValue, $value );
}
wfProfileOut( __METHOD__ );
return $value;
@@ -70,38 +70,30 @@ class SkinTemplate extends Skin {
*/
/**
- * Name of our skin, set in initPage()
- * It probably need to be all lower case.
+ * Name of our skin, it probably needs to be all lower case. Child classes
+ * should override the default.
*/
- var $skinname;
+ var $skinname = 'monobook';
/**
- * Stylesheets set to use
- * Sub directory in ./skins/ where various stylesheets are located
+ * Stylesheets set to use. Subdirectory in skins/ where various stylesheets
+ * are located. Child classes should override the default.
*/
- var $stylename;
+ var $stylename = 'monobook';
/**
- * For QuickTemplate, the name of the subclass which
- * will actually fill the template.
+ * For QuickTemplate, the name of the subclass which will actually fill the
+ * template. Child classes should override the default.
*/
- var $template;
-
- /**#@-*/
+ var $template = 'QuickTemplate';
/**
- * Setup the base parameters...
- * Child classes should override this to set the name,
- * style subdirectory, and template filler callback.
- *
- * @param $out OutputPage
+ * Whether this skin use OutputPage::headElement() to generate the <head>
+ * tag
*/
- function initPage( OutputPage $out ) {
- parent::initPage( $out );
- $this->skinname = 'monobook';
- $this->stylename = 'monobook';
- $this->template = 'QuickTemplate';
- }
+ var $useHeadElement = false;
+
+ /**#@-*/
/**
* Add specific styles for this skin
@@ -110,7 +102,7 @@ class SkinTemplate extends Skin {
*/
function setupSkinUserCss( OutputPage $out ){
$out->addStyle( 'common/shared.css', 'screen' );
- $out->addStyle( 'common/commonPrint.css', 'print' );
+ $out->addStyle( 'common/commonPrint.css', 'print' );
}
/**
@@ -124,7 +116,7 @@ class SkinTemplate extends Skin {
* @return object
* @private
*/
- function setupTemplate( $classname, $repository=false, $cache_dir=false ) {
+ function setupTemplate( $classname, $repository = false, $cache_dir = false ) {
return new $classname();
}
@@ -134,15 +126,15 @@ class SkinTemplate extends Skin {
* @param $out OutputPage
*/
function outputPage( OutputPage $out ) {
- global $wgTitle, $wgArticle, $wgUser, $wgLang, $wgContLang;
+ global $wgArticle, $wgUser, $wgLang, $wgContLang;
global $wgScript, $wgStylePath, $wgContLanguageCode;
global $wgMimeType, $wgJsMimeType, $wgOutputEncoding, $wgRequest;
- global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces;
+ global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces, $wgHtml5Version;
global $wgDisableCounters, $wgLogo, $wgHideInterlanguageLinks;
global $wgMaxCredits, $wgShowCreditsIfMax;
global $wgPageShowWatchingUsers;
- global $wgUseTrackbacks, $wgUseSiteJs;
- global $wgArticlePath, $wgScriptPath, $wgServer, $wgLang, $wgCanonicalNamespaceNames;
+ global $wgUseTrackbacks, $wgUseSiteJs, $wgDebugComments;
+ global $wgArticlePath, $wgScriptPath, $wgServer;
wfProfileIn( __METHOD__ );
@@ -150,23 +142,31 @@ class SkinTemplate extends Skin {
$diff = $wgRequest->getVal( 'diff' );
$action = $wgRequest->getVal( 'action', 'view' );
- wfProfileIn( __METHOD__."-init" );
+ wfProfileIn( __METHOD__ . '-init' );
$this->initPage( $out );
$this->setMembers();
$tpl = $this->setupTemplate( $this->template, 'skins' );
#if ( $wgUseDatabaseMessages ) { // uncomment this to fall back to GetText
- $tpl->setTranslator(new MediaWiki_I18N());
+ $tpl->setTranslator( new MediaWiki_I18N() );
#}
- wfProfileOut( __METHOD__."-init" );
+ wfProfileOut( __METHOD__ . '-init' );
- wfProfileIn( __METHOD__."-stuff" );
- $this->thispage = $this->mTitle->getPrefixedDbKey();
+ wfProfileIn( __METHOD__ . '-stuff' );
+ $this->thispage = $this->mTitle->getPrefixedDBkey();
$this->thisurl = $this->mTitle->getPrefixedURL();
+ $query = array();
+ if ( !$wgRequest->wasPosted() ) {
+ $query = $wgRequest->getValues();
+ unset( $query['title'] );
+ unset( $query['returnto'] );
+ unset( $query['returntoquery'] );
+ }
+ $this->thisquery = wfUrlencode( wfArrayToCGI( $query ) );
$this->loggedin = $wgUser->isLoggedIn();
- $this->iscontent = ($this->mTitle->getNamespace() != NS_SPECIAL );
- $this->iseditable = ($this->iscontent and !($action == 'edit' or $action == 'submit'));
+ $this->iscontent = ( $this->mTitle->getNamespace() != NS_SPECIAL );
+ $this->iseditable = ( $this->iscontent and !( $action == 'edit' or $action == 'submit' ) );
$this->username = $wgUser->getName();
if ( $wgUser->isLoggedIn() || $this->showIPinHeader() ) {
@@ -177,22 +177,59 @@ class SkinTemplate extends Skin {
$this->userpageUrlDetails = self::makeKnownUrlDetails( $this->userpage );
}
- $this->userjs = $this->userjsprev = false;
- $this->setupUserCss( $out );
- $this->setupUserJs( $out->isUserJsAllowed() );
$this->titletxt = $this->mTitle->getPrefixedText();
- wfProfileOut( __METHOD__."-stuff" );
+ wfProfileOut( __METHOD__ . '-stuff' );
- wfProfileIn( __METHOD__."-stuff2" );
+ wfProfileIn( __METHOD__ . '-stuff-head' );
+ if ( $this->useHeadElement ) {
+ $pagecss = $this->setupPageCss();
+ if( $pagecss )
+ $out->addInlineStyle( $pagecss );
+ } else {
+ $this->setupUserCss( $out );
+
+ $tpl->set( 'pagecss', $this->setupPageCss() );
+ $tpl->setRef( 'usercss', $this->usercss );
+
+ $this->userjs = $this->userjsprev = false;
+ $this->setupUserJs( $out->isUserJsAllowed() );
+ $tpl->setRef( 'userjs', $this->userjs );
+ $tpl->setRef( 'userjsprev', $this->userjsprev );
+
+ if( $wgUseSiteJs ) {
+ $jsCache = $this->loggedin ? '&smaxage=0' : '';
+ $tpl->set( 'jsvarurl',
+ self::makeUrl( '-',
+ "action=raw$jsCache&gen=js&useskin=" .
+ urlencode( $this->getSkinName() ) ) );
+ } else {
+ $tpl->set( 'jsvarurl', false );
+ }
+
+ $tpl->setRef( 'xhtmldefaultnamespace', $wgXhtmlDefaultNamespace );
+ $tpl->set( 'xhtmlnamespaces', $wgXhtmlNamespaces );
+ $tpl->set( 'html5version', $wgHtml5Version );
+ $tpl->set( 'headlinks', $out->getHeadLinks() );
+ $tpl->set( 'csslinks', $out->buildCssLinks() );
+
+ if( $wgUseTrackbacks && $out->isArticleRelated() ) {
+ $tpl->set( 'trackbackhtml', $out->getTitle()->trackbackRDF() );
+ } else {
+ $tpl->set( 'trackbackhtml', null );
+ }
+ }
+ wfProfileOut( __METHOD__ . '-stuff-head' );
+
+ wfProfileIn( __METHOD__ . '-stuff2' );
$tpl->set( 'title', $out->getPageTitle() );
$tpl->set( 'pagetitle', $out->getHTMLTitle() );
$tpl->set( 'displaytitle', $out->mPageLinkTitle );
$tpl->set( 'pageclass', $this->getPageClasses( $this->mTitle ) );
- $tpl->set( 'skinnameclass', ( "skin-" . Sanitizer::escapeClass( $this->getSkinName ( ) ) ) );
+ $tpl->set( 'skinnameclass', ( 'skin-' . Sanitizer::escapeClass( $this->getSkinName() ) ) );
- $nsname = isset( $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] ) ?
- $wgCanonicalNamespaceNames[ $this->mTitle->getNamespace() ] :
- $this->mTitle->getNsText();
+ $nsname = MWNamespace::exists( $this->mTitle->getNamespace() ) ?
+ MWNamespace::getCanonicalName( $this->mTitle->getNamespace() ) :
+ $this->mTitle->getNsText();
$tpl->set( 'nscanonical', $nsname );
$tpl->set( 'nsnumber', $this->mTitle->getNamespace() );
@@ -203,54 +240,45 @@ class SkinTemplate extends Skin {
$tpl->set( 'isarticle', $out->isArticle() );
- $tpl->setRef( "thispage", $this->thispage );
+ $tpl->setRef( 'thispage', $this->thispage );
$subpagestr = $this->subPageSubtitle();
$tpl->set(
- 'subtitle', !empty($subpagestr)?
- '<span class="subpages">'.$subpagestr.'</span>'.$out->getSubtitle():
+ 'subtitle', !empty( $subpagestr ) ?
+ '<span class="subpages">'.$subpagestr.'</span>'.$out->getSubtitle() :
$out->getSubtitle()
);
$undelete = $this->getUndeleteLink();
$tpl->set(
- "undelete", !empty($undelete)?
- '<span class="subpages">'.$undelete.'</span>':
+ 'undelete', !empty( $undelete ) ?
+ '<span class="subpages">'.$undelete.'</span>' :
''
);
- $tpl->set( 'catlinks', $this->getCategories());
+ $tpl->set( 'catlinks', $this->getCategories() );
if( $out->isSyndicated() ) {
$feeds = array();
foreach( $out->getSyndicationLinks() as $format => $link ) {
$feeds[$format] = array(
'text' => wfMsg( "feed-$format" ),
- 'href' => $link );
+ 'href' => $link
+ );
}
$tpl->setRef( 'feeds', $feeds );
} else {
$tpl->set( 'feeds', false );
}
- if ($wgUseTrackbacks && $out->isArticleRelated()) {
- $tpl->set( 'trackbackhtml', $wgTitle->trackbackRDF() );
- } else {
- $tpl->set( 'trackbackhtml', null );
- }
- $tpl->setRef( 'xhtmldefaultnamespace', $wgXhtmlDefaultNamespace );
- $tpl->set( 'xhtmlnamespaces', $wgXhtmlNamespaces );
$tpl->setRef( 'mimetype', $wgMimeType );
$tpl->setRef( 'jsmimetype', $wgJsMimeType );
$tpl->setRef( 'charset', $wgOutputEncoding );
- $tpl->set( 'headlinks', $out->getHeadLinks() );
- $tpl->set( 'headscripts', $out->getScript() );
- $tpl->set( 'csslinks', $out->buildCssLinks() );
$tpl->setRef( 'wgScript', $wgScript );
$tpl->setRef( 'skinname', $this->skinname );
$tpl->set( 'skinclass', get_class( $this ) );
$tpl->setRef( 'stylename', $this->stylename );
- $tpl->set( 'printable', $wgRequest->getBool( 'printable' ) );
+ $tpl->set( 'printable', $out->isPrintable() );
$tpl->set( 'handheld', $wgRequest->getBool( 'handheld' ) );
$tpl->setRef( 'loggedin', $this->loggedin );
- $tpl->set('notspecialpage', $this->mTitle->getNamespace() != NS_SPECIAL);
+ $tpl->set( 'notspecialpage', $this->mTitle->getNamespace() != NS_SPECIAL );
/* XXX currently unused, might get useful later
$tpl->set( "editable", ($this->mTitle->getNamespace() != NS_SPECIAL ) );
$tpl->set( "exists", $this->mTitle->getArticleID() != 0 );
@@ -259,111 +287,130 @@ class SkinTemplate extends Skin {
$tpl->set( "helppage", wfMsg('helppage'));
*/
$tpl->set( 'searchaction', $this->escapeSearchLink() );
- $tpl->set( 'searchtitle', SpecialPage::getTitleFor('search')->getPrefixedDBKey() );
+ $tpl->set( 'searchtitle', SpecialPage::getTitleFor( 'Search' )->getPrefixedDBKey() );
$tpl->set( 'search', trim( $wgRequest->getVal( 'search' ) ) );
$tpl->setRef( 'stylepath', $wgStylePath );
$tpl->setRef( 'articlepath', $wgArticlePath );
$tpl->setRef( 'scriptpath', $wgScriptPath );
$tpl->setRef( 'serverurl', $wgServer );
$tpl->setRef( 'logopath', $wgLogo );
- $tpl->setRef( "lang", $wgContLanguageCode );
- $tpl->set( 'dir', $wgContLang->isRTL() ? "rtl" : "ltr" );
+ $tpl->setRef( 'lang', $wgContLanguageCode );
+ $tpl->set( 'dir', $wgContLang->getDir() );
$tpl->set( 'rtl', $wgContLang->isRTL() );
+ $tpl->set( 'capitalizeallnouns', $wgLang->capitalizeAllNouns() ? ' capitalize-all-nouns' : '' );
$tpl->set( 'langname', $wgContLang->getLanguageName( $wgContLanguageCode ) );
$tpl->set( 'showjumplinks', $wgUser->getOption( 'showjumplinks' ) );
- $tpl->set( 'username', $wgUser->isAnon() ? NULL : $this->username );
- $tpl->setRef( 'userpage', $this->userpage);
- $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href']);
+ $tpl->set( 'username', $wgUser->isAnon() ? null : $this->username );
+ $tpl->setRef( 'userpage', $this->userpage );
+ $tpl->setRef( 'userpageurl', $this->userpageUrlDetails['href'] );
$tpl->set( 'userlang', $wgLang->getCode() );
- $tpl->set( 'pagecss', $this->setupPageCss() );
- $tpl->setRef( 'usercss', $this->usercss);
- $tpl->setRef( 'userjs', $this->userjs);
- $tpl->setRef( 'userjsprev', $this->userjsprev);
- if( $wgUseSiteJs ) {
- $jsCache = $this->loggedin ? '&smaxage=0' : '';
- $tpl->set( 'jsvarurl',
- self::makeUrl('-',
- "action=raw$jsCache&gen=js&useskin=" .
- urlencode( $this->getSkinName() ) ) );
- } else {
- $tpl->set('jsvarurl', false);
+
+ // Users can have their language set differently than the
+ // content of the wiki. For these users, tell the web browser
+ // that interface elements are in a different language.
+ $tpl->set( 'userlangattributes', '');
+ $tpl->set( 'specialpageattributes', '');
+
+ $lang = $wgLang->getCode();
+ $dir = $wgLang->getDir();
+ if ( $lang !== $wgContLang->getCode() || $dir !== $wgContLang->getDir() ) {
+ $attrs = " lang='$lang' dir='$dir'";
+
+ $tpl->set( 'userlangattributes', $attrs );
+
+ // The content of SpecialPages should be presented in the
+ // user's language. Content of regular pages should not be touched.
+ if($this->mTitle->isSpecialPage()) {
+ $tpl->set( 'specialpageattributes', $attrs );
+ }
}
+
$newtalks = $wgUser->getNewMessageLinks();
+ $ntl = '';
- if (count($newtalks) == 1 && $newtalks[0]["wiki"] === wfWikiID() ) {
+ if( count( $newtalks ) == 1 && $newtalks[0]['wiki'] === wfWikiID() ) {
$usertitle = $this->mUser->getUserPage();
$usertalktitle = $usertitle->getTalkPage();
+
if( !$usertalktitle->equals( $this->mTitle ) ) {
- $ntl = wfMsg( 'youhavenewmessages',
- $this->makeKnownLinkObj(
- $usertalktitle,
- wfMsgHtml( 'newmessageslink' ),
- 'redirect=no'
- ),
- $this->makeKnownLinkObj(
- $usertalktitle,
- wfMsgHtml( 'newmessagesdifflink' ),
- 'diff=cur'
- )
+ $newmessageslink = $this->link(
+ $usertalktitle,
+ wfMsgHtml( 'newmessageslink' ),
+ array(),
+ array( 'redirect' => 'no' ),
+ array( 'known', 'noclasses' )
+ );
+
+ $newmessagesdifflink = $this->link(
+ $usertalktitle,
+ wfMsgHtml( 'newmessagesdifflink' ),
+ array(),
+ array( 'diff' => 'cur' ),
+ array( 'known', 'noclasses' )
+ );
+
+ $ntl = wfMsg(
+ 'youhavenewmessages',
+ $newmessageslink,
+ $newmessagesdifflink
);
# Disable Cache
- $out->setSquidMaxage(0);
+ $out->setSquidMaxage( 0 );
}
- } else if (count($newtalks)) {
- $sep = str_replace("_", " ", wfMsgHtml("newtalkseparator"));
+ } else if( count( $newtalks ) ) {
+ // _>" " for BC <= 1.16
+ $sep = str_replace( '_', ' ', wfMsgHtml( 'newtalkseparator' ) );
$msgs = array();
- foreach ($newtalks as $newtalk) {
- $msgs[] = Xml::element("a",
- array('href' => $newtalk["link"]), $newtalk["wiki"]);
+ foreach( $newtalks as $newtalk ) {
+ $msgs[] = Xml::element('a',
+ array( 'href' => $newtalk['link'] ), $newtalk['wiki'] );
}
- $parts = implode($sep, $msgs);
- $ntl = wfMsgHtml('youhavenewmessagesmulti', $parts);
- $out->setSquidMaxage(0);
- } else {
- $ntl = '';
+ $parts = implode( $sep, $msgs );
+ $ntl = wfMsgHtml( 'youhavenewmessagesmulti', $parts );
+ $out->setSquidMaxage( 0 );
}
- wfProfileOut( __METHOD__."-stuff2" );
+ wfProfileOut( __METHOD__ . '-stuff2' );
- wfProfileIn( __METHOD__."-stuff3" );
+ wfProfileIn( __METHOD__ . '-stuff3' );
$tpl->setRef( 'newtalk', $ntl );
$tpl->setRef( 'skin', $this );
$tpl->set( 'logo', $this->logoText() );
- if ( $out->isArticle() and (!isset( $oldid ) or isset( $diff )) and
- $wgArticle and 0 != $wgArticle->getID() )
- {
+ if ( $out->isArticle() and ( !isset( $oldid ) or isset( $diff ) ) and
+ $wgArticle and 0 != $wgArticle->getID() ){
if ( !$wgDisableCounters ) {
$viewcount = $wgLang->formatNum( $wgArticle->getCount() );
if ( $viewcount ) {
- $tpl->set('viewcount', wfMsgExt( 'viewcount', array( 'parseinline' ), $viewcount ) );
+ $tpl->set( 'viewcount', wfMsgExt( 'viewcount', array( 'parseinline' ), $viewcount ) );
} else {
- $tpl->set('viewcount', false);
+ $tpl->set( 'viewcount', false );
}
} else {
- $tpl->set('viewcount', false);
+ $tpl->set( 'viewcount', false );
}
- if ($wgPageShowWatchingUsers) {
+ if( $wgPageShowWatchingUsers ) {
$dbr = wfGetDB( DB_SLAVE );
$watchlist = $dbr->tableName( 'watchlist' );
- $sql = "SELECT COUNT(*) AS n FROM $watchlist
- WHERE wl_title='" . $dbr->strencode($this->mTitle->getDBkey()) .
- "' AND wl_namespace=" . $this->mTitle->getNamespace() ;
- $res = $dbr->query( $sql, 'SkinTemplate::outputPage');
+ $res = $dbr->select( 'watchlist',
+ array( 'COUNT(*) AS n' ),
+ array( 'wl_title' => $dbr->strencode( $this->mTitle->getDBkey() ), 'wl_namespace' => $this->mTitle->getNamespace() ),
+ __METHOD__
+ );
$x = $dbr->fetchObject( $res );
$numberofwatchingusers = $x->n;
- if ($numberofwatchingusers > 0) {
- $tpl->set('numberofwatchingusers',
- wfMsgExt('number_of_watching_users_pageview', array('parseinline'),
- $wgLang->formatNum($numberofwatchingusers))
+ if( $numberofwatchingusers > 0 ) {
+ $tpl->set( 'numberofwatchingusers',
+ wfMsgExt( 'number_of_watching_users_pageview', array( 'parseinline' ),
+ $wgLang->formatNum( $numberofwatchingusers ) )
);
} else {
- $tpl->set('numberofwatchingusers', false);
+ $tpl->set( 'numberofwatchingusers', false );
}
} else {
- $tpl->set('numberofwatchingusers', false);
+ $tpl->set( 'numberofwatchingusers', false );
}
- $tpl->set('copyright',$this->getCopyright());
+ $tpl->set( 'copyright', $this->getCopyright() );
$this->credits = false;
@@ -376,28 +423,33 @@ class SkinTemplate extends Skin {
$tpl->setRef( 'credits', $this->credits );
} elseif ( isset( $oldid ) && !isset( $diff ) ) {
- $tpl->set('copyright', $this->getCopyright());
- $tpl->set('viewcount', false);
- $tpl->set('lastmod', false);
- $tpl->set('credits', false);
- $tpl->set('numberofwatchingusers', false);
+ $tpl->set( 'copyright', $this->getCopyright() );
+ $tpl->set( 'viewcount', false );
+ $tpl->set( 'lastmod', false );
+ $tpl->set( 'credits', false );
+ $tpl->set( 'numberofwatchingusers', false );
} else {
- $tpl->set('copyright', false);
- $tpl->set('viewcount', false);
- $tpl->set('lastmod', false);
- $tpl->set('credits', false);
- $tpl->set('numberofwatchingusers', false);
+ $tpl->set( 'copyright', false );
+ $tpl->set( 'viewcount', false );
+ $tpl->set( 'lastmod', false );
+ $tpl->set( 'credits', false );
+ $tpl->set( 'numberofwatchingusers', false );
}
- wfProfileOut( __METHOD__."-stuff3" );
+ wfProfileOut( __METHOD__ . '-stuff3' );
- wfProfileIn( __METHOD__."-stuff4" );
+ wfProfileIn( __METHOD__ . '-stuff4' );
$tpl->set( 'copyrightico', $this->getCopyrightIcon() );
$tpl->set( 'poweredbyico', $this->getPoweredBy() );
$tpl->set( 'disclaimer', $this->disclaimerLink() );
$tpl->set( 'privacy', $this->privacyLink() );
$tpl->set( 'about', $this->aboutLink() );
- $tpl->setRef( 'debug', $out->mDebugtext );
+ if ( $wgDebugComments ) {
+ $tpl->setRef( 'debug', $out->mDebugtext );
+ } else {
+ $tpl->set( 'debug', '' );
+ }
+
$tpl->set( 'reporttime', wfReportTime() );
$tpl->set( 'sitenotice', wfGetSiteNotice() );
$tpl->set( 'bottomscripts', $this->bottomScripts() );
@@ -413,42 +465,41 @@ class SkinTemplate extends Skin {
foreach( $out->getLanguageLinks() as $l ) {
$tmp = explode( ':', $l, 2 );
$class = 'interwiki-' . $tmp[0];
- unset($tmp);
+ unset( $tmp );
$nt = Title::newFromText( $l );
if ( $nt ) {
$language_urls[] = array(
'href' => $nt->getFullURL(),
- 'text' => ($wgContLang->getLanguageName( $nt->getInterwiki()) != ''?$wgContLang->getLanguageName( $nt->getInterwiki()) : $l),
+ 'text' => ( $wgContLang->getLanguageName( $nt->getInterwiki() ) != '' ?
+ $wgContLang->getLanguageName( $nt->getInterwiki() ) : $l ),
'class' => $class
);
}
}
}
- if(count($language_urls)) {
- $tpl->setRef( 'language_urls', $language_urls);
+ if( count( $language_urls ) ) {
+ $tpl->setRef( 'language_urls', $language_urls );
} else {
- $tpl->set('language_urls', false);
+ $tpl->set( 'language_urls', false );
}
- wfProfileOut( __METHOD__."-stuff4" );
+ wfProfileOut( __METHOD__ . '-stuff4' );
- wfProfileIn( __METHOD__."-stuff5" );
+ wfProfileIn( __METHOD__ . '-stuff5' );
# Personal toolbar
- $tpl->set('personal_urls', $this->buildPersonalUrls());
+ $tpl->set( 'personal_urls', $this->buildPersonalUrls() );
$content_actions = $this->buildContentActionUrls();
- $tpl->setRef('content_actions', $content_actions);
+ $tpl->setRef( 'content_actions', $content_actions );
- // XXX: attach this from javascript, same with section editing
- if($this->iseditable && $wgUser->getOption("editondblclick") )
- {
- $encEditUrl = Xml::escapeJsString( $this->mTitle->getLocalUrl( $this->editUrlOptions() ) );
- $tpl->set('body_ondblclick', 'document.location = "' . $encEditUrl . '";');
- } else {
- $tpl->set('body_ondblclick', false);
- }
- $tpl->set( 'body_onload', false );
$tpl->set( 'sidebar', $this->buildSidebar() );
$tpl->set( 'nav_urls', $this->buildNavUrls() );
+ // Set the head scripts near the end, in case the above actions resulted in added scripts
+ if ( $this->useHeadElement ) {
+ $tpl->set( 'headelement', $out->headElement( $this ) );
+ } else {
+ $tpl->set( 'headscripts', $out->getScript() );
+ }
+
// original version by hansm
if( !wfRunHooks( 'SkinTemplateOutputPageBeforeExec', array( &$this, &$tpl ) ) ) {
wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" );
@@ -456,13 +507,13 @@ class SkinTemplate extends Skin {
// allow extensions adding stuff after the page content.
// See Skin::afterContentHook() for further documentation.
- $tpl->set ('dataAfterContent', $this->afterContentHook());
- wfProfileOut( __METHOD__."-stuff5" );
+ $tpl->set( 'dataAfterContent', $this->afterContentHook() );
+ wfProfileOut( __METHOD__ . '-stuff5' );
// execute template
- wfProfileIn( __METHOD__."-execute" );
+ wfProfileIn( __METHOD__ . '-execute' );
$res = $tpl->execute();
- wfProfileOut( __METHOD__."-execute" );
+ wfProfileOut( __METHOD__ . '-execute' );
// result may be an error
$this->printOrError( $res );
@@ -487,25 +538,31 @@ class SkinTemplate extends Skin {
* @private
*/
function buildPersonalUrls() {
- global $wgTitle, $wgRequest;
+ global $wgOut, $wgRequest;
- $pageurl = $wgTitle->getLocalURL();
+ $title = $wgOut->getTitle();
+ $pageurl = $title->getLocalURL();
wfProfileIn( __METHOD__ );
/* set up the default links for the personal toolbar */
$personal_urls = array();
- if ($this->loggedin) {
+ $page = $wgRequest->getVal( 'returnto', $this->thisurl );
+ $query = $wgRequest->getVal( 'returntoquery', $this->thisquery );
+ $returnto = "returnto=$page";
+ if( $this->thisquery != '' )
+ $returnto .= "&returntoquery=$query";
+ if( $this->loggedin ) {
$personal_urls['userpage'] = array(
'text' => $this->username,
'href' => &$this->userpageUrlDetails['href'],
- 'class' => $this->userpageUrlDetails['exists']?false:'new',
+ 'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
'active' => ( $this->userpageUrlDetails['href'] == $pageurl )
);
- $usertalkUrlDetails = $this->makeTalkUrlDetails($this->userpage);
+ $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage );
$personal_urls['mytalk'] = array(
- 'text' => wfMsg('mytalk'),
+ 'text' => wfMsg( 'mytalk' ),
'href' => &$usertalkUrlDetails['href'],
- 'class' => $usertalkUrlDetails['exists']?false:'new',
+ 'class' => $usertalkUrlDetails['exists'] ? false : 'new',
'active' => ( $usertalkUrlDetails['href'] == $pageurl )
);
$href = self::makeSpecialUrl( 'Preferences' );
@@ -526,10 +583,10 @@ class SkinTemplate extends Skin {
# 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 ) {
+ $origTitle = Title::newFromText( $wgRequest->getText( 'title' ) );
+ if( $origTitle instanceof Title && $origTitle->getNamespace() == NS_SPECIAL ) {
list( $spName, $spPar ) =
- SpecialPage::resolveAliasWithSubpage( $title->getText() );
+ SpecialPage::resolveAliasWithSubpage( $origTitle->getText() );
$active = $spName == 'Contributions'
&& ( ( $spPar && $spPar == $this->username )
|| $wgRequest->getText( 'target' ) == $this->username );
@@ -546,7 +603,7 @@ class SkinTemplate extends Skin {
$personal_urls['logout'] = array(
'text' => wfMsg( 'userlogout' ),
'href' => self::makeSpecialUrl( 'Userlogout',
- $wgTitle->isSpecial( 'Preferences' ) ? '' : "returnto={$this->thisurl}"
+ $title->isSpecial( 'Preferences' ) ? '' : $returnto
),
'active' => false
);
@@ -560,38 +617,37 @@ class SkinTemplate extends Skin {
$personal_urls['anonuserpage'] = array(
'text' => $this->username,
'href' => $href,
- 'class' => $this->userpageUrlDetails['exists']?false:'new',
+ 'class' => $this->userpageUrlDetails['exists'] ? false : 'new',
'active' => ( $pageurl == $href )
);
- $usertalkUrlDetails = $this->makeTalkUrlDetails($this->userpage);
+ $usertalkUrlDetails = $this->makeTalkUrlDetails( $this->userpage );
$href = &$usertalkUrlDetails['href'];
$personal_urls['anontalk'] = array(
- 'text' => wfMsg('anontalk'),
+ 'text' => wfMsg( 'anontalk' ),
'href' => $href,
- 'class' => $usertalkUrlDetails['exists']?false:'new',
+ 'class' => $usertalkUrlDetails['exists'] ? false : 'new',
'active' => ( $pageurl == $href )
);
$personal_urls['anonlogin'] = array(
'text' => wfMsg( $loginlink ),
- 'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ),
- 'active' => $wgTitle->isSpecial( 'Userlogin' )
+ 'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
+ 'active' => $title->isSpecial( 'Userlogin' )
);
} else {
-
$personal_urls['login'] = array(
'text' => wfMsg( $loginlink ),
- 'href' => self::makeSpecialUrl( 'Userlogin', 'returnto=' . $this->thisurl ),
- 'active' => $wgTitle->isSpecial( 'Userlogin' )
+ 'href' => self::makeSpecialUrl( 'Userlogin', $returnto ),
+ 'active' => $title->isSpecial( 'Userlogin' )
);
}
}
- wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$wgTitle ) );
+ wfRunHooks( 'PersonalUrls', array( &$personal_urls, &$title ) );
wfProfileOut( __METHOD__ );
return $personal_urls;
}
- function tabAction( $title, $message, $selected, $query='', $checkEdit=false ) {
+ function tabAction( $title, $message, $selected, $query = '', $checkEdit = false ) {
$classes = array();
if( $selected ) {
$classes[] = 'selected';
@@ -608,9 +664,9 @@ class SkinTemplate extends Skin {
}
$result = array();
- if( !wfRunHooks('SkinTemplateTabAction', array(&$this,
+ if( !wfRunHooks( 'SkinTemplateTabAction', array( &$this,
$title, $message, $selected, $checkEdit,
- &$classes, &$query, &$text, &$result)) ) {
+ &$classes, &$query, &$text, &$result ) ) ) {
return $result;
}
@@ -622,8 +678,8 @@ class SkinTemplate extends Skin {
function makeTalkUrlDetails( $name, $urlaction = '' ) {
$title = Title::newFromText( $name );
- if( !is_object($title) ) {
- throw new MWException( __METHOD__." given invalid pagename $name" );
+ if( !is_object( $title ) ) {
+ throw new MWException( __METHOD__ . " given invalid pagename $name" );
}
$title = $title->getTalkPage();
self::checkTitle( $title, $name );
@@ -649,7 +705,7 @@ class SkinTemplate extends Skin {
* @private
*/
function buildContentActionUrls() {
- global $wgContLang, $wgLang, $wgOut, $wgUser, $wgRequest;
+ global $wgContLang, $wgLang, $wgOut, $wgUser, $wgRequest, $wgArticle;
wfProfileIn( __METHOD__ );
@@ -657,8 +713,8 @@ class SkinTemplate extends Skin {
$section = $wgRequest->getVal( 'section' );
$content_actions = array();
- $prevent_active_tabs = false ;
- wfRunHooks( 'SkinTemplatePreventOtherActiveTabs', array( &$this , &$prevent_active_tabs ) ) ;
+ $prevent_active_tabs = false;
+ wfRunHooks( 'SkinTemplatePreventOtherActiveTabs', array( &$this, &$prevent_active_tabs ) );
if( $this->iscontent ) {
$subjpage = $this->mTitle->getSubjectPage();
@@ -669,32 +725,35 @@ class SkinTemplate extends Skin {
$subjpage,
$nskey,
!$this->mTitle->isTalkPage() && !$prevent_active_tabs,
- '', true);
+ '', true
+ );
$content_actions['talk'] = $this->tabAction(
$talkpage,
'talk',
$this->mTitle->isTalkPage() && !$prevent_active_tabs,
'',
- true);
+ true
+ );
- wfProfileIn( __METHOD__."-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,
+ 'class' => ( ( ( $action == 'edit' or $action == 'submit' ) and $section != 'new' ) ? 'selected' : '' ) . $istalkclass,
'text' => $this->mTitle->exists()
? wfMsg( 'edit' )
: wfMsg( 'create' ),
'href' => $this->mTitle->getLocalUrl( $this->editUrlOptions() )
);
- if ( $istalk || $wgOut->showNewSectionLink() ) {
+ // adds new section link if page is a current revision of a talk page or
+ if ( ( $wgArticle && $wgArticle->isCurrent() && $istalk ) || $wgOut->showNewSectionLink() ) {
if ( !$wgOut->forceHideNewSectionLink() ) {
$content_actions['addsection'] = array(
'class' => $section == 'new' ? 'selected' : false,
- 'text' => wfMsg('addsection'),
+ 'text' => wfMsg( 'addsection' ),
'href' => $this->mTitle->getLocalUrl( 'action=edit&section=new' )
);
}
@@ -702,26 +761,26 @@ class SkinTemplate extends Skin {
} elseif ( $this->mTitle->isKnown() ) {
$content_actions['viewsource'] = array(
'class' => ($action == 'edit') ? 'selected' : false,
- 'text' => wfMsg('viewsource'),
+ 'text' => wfMsg( 'viewsource' ),
'href' => $this->mTitle->getLocalUrl( $this->editUrlOptions() )
);
}
- wfProfileOut( __METHOD__."-edit" );
+ wfProfileOut( __METHOD__ . '-edit' );
- wfProfileIn( __METHOD__."-live" );
+ wfProfileIn( __METHOD__ . '-live' );
if ( $this->mTitle->exists() ) {
$content_actions['history'] = array(
'class' => ($action == 'history') ? 'selected' : false,
- 'text' => wfMsg('history_short'),
+ 'text' => wfMsg( 'history_short' ),
'href' => $this->mTitle->getLocalUrl( 'action=history' ),
'rel' => 'archives',
);
- if( $wgUser->isAllowed('delete') ) {
+ if( $wgUser->isAllowed( 'delete' ) ) {
$content_actions['delete'] = array(
'class' => ($action == 'delete') ? 'selected' : false,
- 'text' => wfMsg('delete'),
+ 'text' => wfMsg( 'delete' ),
'href' => $this->mTitle->getLocalUrl( 'action=delete' )
);
}
@@ -729,7 +788,7 @@ class SkinTemplate extends Skin {
$moveTitle = SpecialPage::getTitleFor( 'Movepage', $this->thispage );
$content_actions['move'] = array(
'class' => $this->mTitle->isSpecial( 'Movepage' ) ? 'selected' : false,
- 'text' => wfMsg('move'),
+ 'text' => wfMsg( 'move' ),
'href' => $moveTitle->getLocalUrl()
);
}
@@ -738,26 +797,26 @@ class SkinTemplate extends Skin {
if( !$this->mTitle->isProtected() ){
$content_actions['protect'] = array(
'class' => ($action == 'protect') ? 'selected' : false,
- 'text' => wfMsg('protect'),
+ 'text' => wfMsg( 'protect' ),
'href' => $this->mTitle->getLocalUrl( 'action=protect' )
);
} else {
$content_actions['unprotect'] = array(
'class' => ($action == 'unprotect') ? 'selected' : false,
- 'text' => wfMsg('unprotect'),
+ 'text' => wfMsg( 'unprotect' ),
'href' => $this->mTitle->getLocalUrl( 'action=unprotect' )
);
}
}
} else {
//article doesn't exist or is deleted
- if( $wgUser->isAllowed( 'deletedhistory' ) && $wgUser->isAllowed( 'undelete' ) ) {
+ if( $wgUser->isAllowed( 'deletedhistory' ) && $wgUser->isAllowed( 'deletedtext' ) ) {
if( $n = $this->mTitle->isDeleted() ) {
$undelTitle = SpecialPage::getTitleFor( 'Undelete' );
$content_actions['undelete'] = array(
'class' => false,
- 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $wgLang->formatNum($n) ),
+ 'text' => wfMsgExt( 'undelete_short', array( 'parsemag' ), $wgLang->formatNum( $n ) ),
'href' => $undelTitle->getLocalUrl( 'target=' . urlencode( $this->thispage ) )
#'href' => self::makeSpecialUrl( "Undelete/$this->thispage" )
);
@@ -768,40 +827,40 @@ class SkinTemplate extends Skin {
if( !$this->mTitle->getRestrictions( 'create' ) ) {
$content_actions['protect'] = array(
'class' => ($action == 'protect') ? 'selected' : false,
- 'text' => wfMsg('protect'),
+ 'text' => wfMsg( 'protect' ),
'href' => $this->mTitle->getLocalUrl( 'action=protect' )
);
} else {
$content_actions['unprotect'] = array(
'class' => ($action == 'unprotect') ? 'selected' : false,
- 'text' => wfMsg('unprotect'),
+ 'text' => wfMsg( 'unprotect' ),
'href' => $this->mTitle->getLocalUrl( 'action=unprotect' )
);
}
}
}
- wfProfileOut( __METHOD__."-live" );
+ wfProfileOut( __METHOD__ . '-live' );
if( $this->loggedin ) {
if( !$this->mTitle->userIsWatching()) {
$content_actions['watch'] = array(
'class' => ($action == 'watch' or $action == 'unwatch') ? 'selected' : false,
- 'text' => wfMsg('watch'),
+ 'text' => wfMsg( 'watch' ),
'href' => $this->mTitle->getLocalUrl( 'action=watch' )
);
} else {
$content_actions['unwatch'] = array(
'class' => ($action == 'unwatch' or $action == 'watch') ? 'selected' : false,
- 'text' => wfMsg('unwatch'),
+ 'text' => wfMsg( 'unwatch' ),
'href' => $this->mTitle->getLocalUrl( 'action=unwatch' )
);
}
}
- wfRunHooks( 'SkinTemplateTabs', array( &$this , &$content_actions ) ) ;
+ wfRunHooks( 'SkinTemplateTabs', array( $this, &$content_actions ) );
} else {
/* show special page tab */
@@ -826,10 +885,10 @@ class SkinTemplate extends Skin {
continue;
$selected = ( $code == $preferred )? 'selected' : false;
$content_actions['varlang-' . $vcount] = array(
- 'class' => $selected,
- 'text' => $varname,
- 'href' => $this->mTitle->getLocalURL('',$code)
- );
+ 'class' => $selected,
+ 'text' => $varname,
+ 'href' => $this->mTitle->getLocalURL( '', $code )
+ );
$vcount ++;
}
}
@@ -840,15 +899,13 @@ class SkinTemplate extends Skin {
return $content_actions;
}
-
-
/**
* build array of common navigation links
* @return array
* @private
*/
function buildNavUrls() {
- global $wgUseTrackbacks, $wgTitle, $wgUser, $wgRequest;
+ global $wgUseTrackbacks, $wgOut, $wgUser, $wgRequest;
global $wgEnableUploads, $wgUploadNavigationUrl;
wfProfileIn( __METHOD__ );
@@ -857,17 +914,12 @@ class SkinTemplate extends Skin {
$nav_urls = array();
$nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() );
- if( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) {
- if ($wgUploadNavigationUrl) {
- $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl );
- } else {
- $nav_urls['upload'] = array( 'href' => self::makeSpecialUrl( 'Upload' ) );
- }
+ if( $wgUploadNavigationUrl ) {
+ $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl );
+ } elseif( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) {
+ $nav_urls['upload'] = array( 'href' => self::makeSpecialUrl( 'Upload' ) );
} else {
- if ($wgUploadNavigationUrl)
- $nav_urls['upload'] = array( 'href' => $wgUploadNavigationUrl );
- else
- $nav_urls['upload'] = false;
+ $nav_urls['upload'] = false;
}
$nav_urls['specialpages'] = array( 'href' => self::makeSpecialUrl( 'Specialpages' ) );
@@ -877,16 +929,18 @@ class SkinTemplate extends Skin {
// A print stylesheet is attached to all pages, but nobody ever
// figures that out. :) Add a link...
if( $this->iscontent && ( $action == 'view' || $action == 'purge' ) ) {
- $nav_urls['print'] = array(
- 'text' => wfMsg( 'printableversion' ),
- 'href' => $wgRequest->appendQuery( 'printable=yes' )
- );
+ if ( !$wgOut->isPrintable() ) {
+ $nav_urls['print'] = array(
+ 'text' => wfMsg( 'printableversion' ),
+ 'href' => $wgRequest->appendQuery( 'printable=yes' )
+ );
+ }
// Also add a "permalink" while we're at it
if ( $this->mRevisionId ) {
$nav_urls['permalink'] = array(
'text' => wfMsg( 'permalink' ),
- 'href' => $wgTitle->getLocalURL( "oldid=$this->mRevisionId" )
+ 'href' => $wgOut->getTitle()->getLocalURL( "oldid=$this->mRevisionId" )
);
}
@@ -908,29 +962,34 @@ class SkinTemplate extends Skin {
} else {
$nav_urls['recentchangeslinked'] = false;
}
- if ($wgUseTrackbacks)
+ if( $wgUseTrackbacks )
$nav_urls['trackbacklink'] = array(
- 'href' => $wgTitle->trackbackURL()
+ 'href' => $wgOut->getTitle()->trackbackURL()
);
}
if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
- $id = User::idFromName($this->mTitle->getText());
- $ip = User::isIP($this->mTitle->getText());
+ $id = User::idFromName( $this->mTitle->getText() );
+ $ip = User::isIP( $this->mTitle->getText() );
} else {
$id = 0;
$ip = false;
}
- if($id || $ip) { # both anons and non-anons have contribs list
+ if( $id || $ip ) { # both anons and non-anons have contribs list
$nav_urls['contributions'] = array(
'href' => self::makeSpecialUrlSubpage( 'Contributions', $this->mTitle->getText() )
);
if( $id ) {
$logPage = SpecialPage::getTitleFor( 'Log' );
- $nav_urls['log'] = array( 'href' => $logPage->getLocalUrl( 'user='
- . $this->mTitle->getPartialUrl() ) );
+ $nav_urls['log'] = array(
+ 'href' => $logPage->getLocalUrl(
+ array(
+ 'user' => $this->mTitle->getText()
+ )
+ )
+ );
} else {
$nav_urls['log'] = false;
}
@@ -971,7 +1030,6 @@ class SkinTemplate extends Skin {
*/
function setupUserJs( $allowUserJs ) {
global $wgRequest, $wgJsMimeType;
-
wfProfileIn( __METHOD__ );
$action = $wgRequest->getVal( 'action', 'view' );
@@ -979,9 +1037,9 @@ class SkinTemplate extends Skin {
if( $allowUserJs && $this->loggedin ) {
if( $this->mTitle->isJsSubpage() and $this->userCanPreview( $action ) ) {
# XXX: additional security check/prompt?
- $this->userjsprev = '/*<![CDATA[*/ ' . $wgRequest->getText('wpTextbox1') . ' /*]]>*/';
+ $this->userjsprev = '/*<![CDATA[*/ ' . $wgRequest->getText( 'wpTextbox1' ) . ' /*]]>*/';
} else {
- $this->userjs = self::makeUrl($this->userpage.'/'.$this->skinname.'.js', 'action=raw&ctype='.$wgJsMimeType);
+ $this->userjs = self::makeUrl( $this->userpage . '/' . $this->skinname . '.js', 'action=raw&ctype=' . $wgJsMimeType );
}
}
wfProfileOut( __METHOD__ );
@@ -1000,6 +1058,10 @@ class SkinTemplate extends Skin {
wfProfileOut( __METHOD__ );
return $out;
}
+
+ public function commonPrintStylesheet() {
+ return false;
+ }
}
/**
@@ -1007,43 +1069,44 @@ class SkinTemplate extends Skin {
* compatible with what we use of PHPTAL 0.7.
* @ingroup Skins
*/
-class QuickTemplate {
+abstract class QuickTemplate {
/**
- * @public
+ * Constructor
*/
- function QuickTemplate() {
+ public function QuickTemplate() {
$this->data = array();
$this->translator = new MediaWiki_I18N();
}
/**
- * @public
+ * Sets the value $value to $name
+ * @param $name
+ * @param $value
*/
- function set( $name, $value ) {
+ public function set( $name, $value ) {
$this->data[$name] = $value;
}
/**
- * @public
+ * @param $name
+ * @param $value
*/
- function setRef($name, &$value) {
+ public function setRef( $name, &$value ) {
$this->data[$name] =& $value;
}
/**
- * @public
+ * @param $t
*/
- function setTranslator( &$t ) {
+ public function setTranslator( &$t ) {
$this->translator = &$t;
}
/**
- * @public
+ * Main function, used by classes that subclass QuickTemplate
+ * to show the actual HTML output
*/
- function execute() {
- echo "Override this function.";
- }
-
+ abstract public function execute();
/**
* @private
@@ -1085,10 +1148,10 @@ class QuickTemplate {
* @private
*/
function msgWiki( $str ) {
- global $wgParser, $wgTitle, $wgOut;
+ global $wgParser, $wgOut;
$text = $this->translator->translate( $str );
- $parserOutput = $wgParser->parse( $text, $wgTitle,
+ $parserOutput = $wgParser->parse( $text, $wgOut->getTitle(),
$wgOut->parserOptions(), true );
echo $parserOutput->getText();
}
@@ -1105,6 +1168,6 @@ class QuickTemplate {
*/
function haveMsg( $str ) {
$msg = $this->translator->translate( $str );
- return ($msg != '-') && ($msg != ''); # ????
+ return ( $msg != '-' ) && ( $msg != '' ); # ????
}
}
diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php
index 31b43839..80e2f7ed 100644
--- a/includes/SpecialPage.php
+++ b/includes/SpecialPage.php
@@ -27,8 +27,7 @@
* page list.
* @ingroup SpecialPage
*/
-class SpecialPage
-{
+class SpecialPage {
/**#@+
* @access private
*/
@@ -90,30 +89,30 @@ class SpecialPage
'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ),
'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ),
'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ),
- 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ),
- 'Shortpages' => array( 'SpecialPage', 'Shortpages' ),
- 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ),
- 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ),
- 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ),
+ 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ),
+ 'Shortpages' => array( 'SpecialPage', 'Shortpages' ),
+ 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ),
+ 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ),
+ 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ),
'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ),
'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ),
- 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ),
+ 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ),
'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ),
- 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ),
+ 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ),
'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ),
'Wantedfiles' => array( 'SpecialPage', 'Wantedfiles' ),
'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ),
'Wantedtemplates' => array( 'SpecialPage', 'Wantedtemplates' ),
# List of pages
- 'Allpages' => 'SpecialAllpages',
- 'Prefixindex' => 'SpecialPrefixindex',
+ 'Allpages' => 'SpecialAllpages',
+ 'Prefixindex' => 'SpecialPrefixindex',
'Categories' => array( 'SpecialPage', 'Categories' ),
'Disambiguations' => array( 'SpecialPage', 'Disambiguations' ),
- 'Listredirects' => array( 'SpecialPage', 'Listredirects' ),
+ 'Listredirects' => array( 'SpecialPage', 'Listredirects' ),
# Login/create account
- 'Userlogin' => array( 'SpecialPage', 'Userlogin' ),
+ 'Userlogin' => array( 'SpecialPage', 'Userlogin' ),
'CreateAccount' => array( 'SpecialRedirectToSpecial', 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) ),
# Users and rights
@@ -121,14 +120,15 @@ class SpecialPage
'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ),
'Resetpass' => 'SpecialResetpass',
'DeletedContributions' => 'DeletedContributionsPage',
- 'Preferences' => array( 'SpecialPage', 'Preferences' ),
- 'Contributions' => 'SpecialContributions',
+ 'Preferences' => 'SpecialPreferences',
+ 'Contributions' => 'SpecialContributions',
'Listgrouprights' => 'SpecialListGroupRights',
- 'Listusers' => array( 'SpecialPage', 'Listusers' ),
+ 'Listusers' => array( 'SpecialPage', 'Listusers' ),
+ 'Activeusers' => 'SpecialActiveUsers',
'Userrights' => 'UserrightsPage',
# Recent changes and logs
- 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ),
+ 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ),
'Log' => array( 'SpecialPage', 'Log' ),
'Watchlist' => array( 'SpecialPage', 'Watchlist' ),
'Newpages' => 'SpecialNewpages',
@@ -141,11 +141,11 @@ class SpecialPage
'Filepath' => array( 'SpecialPage', 'Filepath' ),
'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ),
'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ),
- 'Upload' => array( 'SpecialPage', 'Upload' ),
+ 'Upload' => 'SpecialUpload',
# Wiki data and tools
- 'Statistics' => 'SpecialStatistics',
- 'Allmessages' => array( 'SpecialPage', 'Allmessages' ),
+ 'Statistics' => 'SpecialStatistics',
+ 'Allmessages' => 'SpecialAllmessages',
'Version' => 'SpecialVersion',
'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ),
'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ),
@@ -167,15 +167,15 @@ class SpecialPage
'Export' => 'SpecialExport',
'Import' => 'SpecialImport',
'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ),
- 'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ),
- 'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ),
-
+ 'Whatlinkshere' => 'SpecialWhatlinkshere',
+ 'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ),
+
# Other
'Booksources' => 'SpecialBookSources',
-
+
# Unlisted / redirects
- 'Blankpage' => array( 'UnlistedSpecialPage', 'Blankpage' ),
- 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
+ 'Blankpage' => 'SpecialBlankpage',
+ 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ),
'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ),
'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ),
'Listbots' => array( 'SpecialRedirectToSpecial', 'Listbots', 'Listusers', 'bot' ),
@@ -277,7 +277,7 @@ class SpecialPage
$bits = explode( '/', $alias, 2 );
$name = self::resolveAlias( $bits[0] );
if( !isset( $bits[1] ) ) { // bug 2087
- $par = NULL;
+ $par = null;
} else {
$par = $bits[1];
}
@@ -394,7 +394,7 @@ class SpecialPage
}
return self::$mList[$name];
} else {
- return NULL;
+ return null;
}
}
@@ -407,7 +407,7 @@ class SpecialPage
if ( $realName ) {
return self::getPage( $realName );
} else {
- return NULL;
+ return null;
}
}
@@ -500,7 +500,7 @@ class SpecialPage
$bits = explode( '/', $title->getDBkey(), 2 );
$name = $bits[0];
if( !isset( $bits[1] ) ) { // bug 2087
- $par = NULL;
+ $par = null;
} else {
$par = $bits[1];
}
@@ -574,6 +574,7 @@ class SpecialPage
$oldTitle = $wgTitle;
$oldOut = $wgOut;
$wgOut = new OutputPage;
+ $wgOut->setTitle( $title );
$ret = SpecialPage::executePath( $title, true );
if ( $ret === true ) {
@@ -597,11 +598,25 @@ class SpecialPage
$aliases = $wgContLang->getSpecialPageAliases();
if ( isset( $aliases[$name][0] ) ) {
$name = $aliases[$name][0];
+ } else {
+ // Try harder in case someone misspelled the correct casing
+ $found = false;
+ foreach ( $aliases as $n => $values ) {
+ if ( strcasecmp( $name, $n ) === 0 ) {
+ wfWarn( "Found alias defined for $n when searching for special page aliases
+for $name. Case mismatch?" );
+ $name = $values[0];
+ $found = true;
+ break;
+ }
+ }
+ if ( !$found ) wfWarn( "Did not find alias for special page '$name'.
+Perhaps no page aliases are defined for it?" );
}
if ( $subpage !== false && !is_null( $subpage ) ) {
$name = "$name/$subpage";
}
- return ucfirst( $name );
+ return $wgContLang->ucfirst( $name );
}
/**
@@ -688,13 +703,18 @@ class SpecialPage
/**#@+
* Accessor and mutator
*/
- function name( $x = NULL ) { return wfSetVar( $this->mName, $x ); }
- function restrictions( $x = NULL) { return wfSetVar( $this->mRestrictions, $x ); }
- function listed( $x = NULL) { return wfSetVar( $this->mListed, $x ); }
- function func( $x = NULL) { return wfSetVar( $this->mFunction, $x ); }
- function file( $x = NULL) { return wfSetVar( $this->mFile, $x ); }
- function includable( $x = NULL ) { return wfSetVar( $this->mIncludable, $x ); }
- function including( $x = NULL ) { return wfSetVar( $this->mIncluding, $x ); }
+ function name( $x = null ) { return wfSetVar( $this->mName, $x ); }
+ function restrictions( $x = null) {
+ # Use the one below this
+ wfDeprecated( __METHOD__ );
+ return wfSetVar( $this->mRestriction, $x );
+ }
+ function restriction( $x = null) { return wfSetVar( $this->mRestriction, $x ); }
+ function listed( $x = null) { return wfSetVar( $this->mListed, $x ); }
+ function func( $x = null) { return wfSetVar( $this->mFunction, $x ); }
+ function file( $x = null) { return wfSetVar( $this->mFile, $x ); }
+ function includable( $x = null ) { return wfSetVar( $this->mIncludable, $x ); }
+ function including( $x = null ) { return wfSetVar( $this->mIncluding, $x ); }
/**#@-*/
/**
@@ -778,7 +798,7 @@ class SpecialPage
* Outputs a summary message on top of special pages
* Per default the message key is the canonical name of the special page
* May be overriden, i.e. by extensions to stick with the naming conventions
- * for message keys: 'extensionname-xxx'
+ * for message keys: 'extensionname-xxx'
*
* @param string message key of the summary
*/
@@ -809,7 +829,7 @@ class SpecialPage
/**
* Get a self-referential title object
*/
- function getTitle( $subpage = false) {
+ function getTitle( $subpage = false ) {
return self::getTitleFor( $this->mName, $subpage );
}
@@ -838,7 +858,7 @@ class SpecialPage
global $wgRequest;
$params = array();
foreach( $this->mAllowedRedirectParams as $arg ) {
- if( $val = $wgRequest->getVal( $arg, false ) )
+ if( ( $val = $wgRequest->getVal( $arg, null ) ) !== null )
$params[] = $arg . '=' . $val;
}
@@ -945,6 +965,8 @@ class SpecialMytalk extends UnlistedSpecialPage {
class SpecialMycontributions extends UnlistedSpecialPage {
function __construct() {
parent::__construct( 'Mycontributions' );
+ $this->mAllowedRedirectParams = array( 'limit', 'namespace', 'tagfilter',
+ 'offset', 'dir', 'year', 'month', 'feed' );
}
function getRedirect( $subpage ) {
diff --git a/includes/SquidPurgeClient.php b/includes/SquidPurgeClient.php
new file mode 100644
index 00000000..65da5c1a
--- /dev/null
+++ b/includes/SquidPurgeClient.php
@@ -0,0 +1,380 @@
+<?php
+/**
+ * An HTTP 1.0 client built for the purposes of purging Squid and Varnish.
+ * Uses asynchronous I/O, allowing purges to be done in a highly parallel
+ * manner.
+ *
+ * Could be replaced by curl_multi_exec() or some such.
+ */
+class SquidPurgeClient {
+ var $host, $port, $ip;
+
+ var $readState = 'idle';
+ var $writeBuffer = '';
+ var $requests = array();
+ var $currentRequestIndex;
+
+ const EINTR = 4;
+ const EAGAIN = 11;
+ const EINPROGRESS = 115;
+ const BUFFER_SIZE = 8192;
+
+ /**
+ * The socket resource, or null for unconnected, or false for disabled due to error
+ */
+ var $socket;
+
+ public function __construct( $server, $options = array() ) {
+ $parts = explode( ':', $server, 2 );
+ $this->host = $parts[0];
+ $this->port = isset( $parts[1] ) ? $parts[1] : 80;
+ }
+
+ /**
+ * Open a socket if there isn't one open already, return it.
+ * Returns false on error.
+ */
+ protected function getSocket() {
+ if ( $this->socket !== null ) {
+ return $this->socket;
+ }
+
+ $ip = $this->getIP();
+ if ( !$ip ) {
+ $this->log( "DNS error" );
+ $this->markDown();
+ return false;
+ }
+ $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
+ socket_set_nonblock( $this->socket );
+ wfSuppressWarnings();
+ $ok = socket_connect( $this->socket, $ip, $this->port );
+ wfRestoreWarnings();
+ if ( !$ok ) {
+ $error = socket_last_error( $this->socket );
+ if ( $error !== self::EINPROGRESS ) {
+ $this->log( "connection error: " . socket_strerror( $error ) );
+ $this->markDown();
+ return false;
+ }
+ }
+
+ return $this->socket;
+ }
+
+ /**
+ * Get read socket array for select()
+ */
+ public function getReadSocketsForSelect() {
+ if ( $this->readState == 'idle' ) {
+ return array();
+ }
+ $socket = $this->getSocket();
+ if ( $socket === false ) {
+ return array();
+ }
+ return array( $socket );
+ }
+
+ /**
+ * Get write socket array for select()
+ */
+ public function getWriteSocketsForSelect() {
+ if ( !strlen( $this->writeBuffer ) ) {
+ return array();
+ }
+ $socket = $this->getSocket();
+ if ( $socket === false ) {
+ return array();
+ }
+ return array( $socket );
+ }
+
+ /**
+ * Get the host's IP address.
+ * Does not support IPv6 at present due to the lack of a convenient interface in PHP.
+ */
+ protected function getIP() {
+ if ( $this->ip === null ) {
+ if ( IP::isIPv4( $this->host ) ) {
+ $this->ip = $this->host;
+ } elseif ( IP::isIPv6( $this->host ) ) {
+ throw new MWException( '$wgSquidServers does not support IPv6' );
+ } else {
+ wfSuppressWarnings();
+ $this->ip = gethostbyname( $this->host );
+ if ( $this->ip === $this->host ) {
+ $this->ip = false;
+ }
+ wfRestoreWarnings();
+ }
+ }
+ return $this->ip;
+ }
+
+ /**
+ * Close the socket and ignore any future purge requests.
+ * This is called if there is a protocol error.
+ */
+ protected function markDown() {
+ $this->close();
+ $this->socket = false;
+ }
+
+ /**
+ * Close the socket but allow it to be reopened for future purge requests
+ */
+ public function close() {
+ if ( $this->socket ) {
+ wfSuppressWarnings();
+ socket_set_block( $this->socket );
+ socket_shutdown( $this->socket );
+ socket_close( $this->socket );
+ wfRestoreWarnings();
+ }
+ $this->socket = null;
+ $this->readBuffer = '';
+ // Write buffer is kept since it may contain a request for the next socket
+ }
+
+ /**
+ * Queue a purge operation
+ */
+ public function queuePurge( $url ) {
+ $url = str_replace( "\n", '', $url );
+ $this->requests[] = "PURGE $url HTTP/1.0\r\n" .
+ "Connection: Keep-Alive\r\n" .
+ "Proxy-Connection: Keep-Alive\r\n" .
+ "User-Agent: " . Http::userAgent() . ' ' . __CLASS__ . "\r\n\r\n";
+ if ( $this->currentRequestIndex === null ) {
+ $this->nextRequest();
+ }
+ }
+
+ public function isIdle() {
+ return strlen( $this->writeBuffer ) == 0 && $this->readState == 'idle';
+ }
+
+ /**
+ * Perform pending writes. Call this when socket_select() indicates that writing will not block.
+ */
+ public function doWrites() {
+ if ( !strlen( $this->writeBuffer ) ) {
+ return;
+ }
+ $socket = $this->getSocket();
+ if ( !$socket ) {
+ return;
+ }
+
+ if ( strlen( $this->writeBuffer ) <= self::BUFFER_SIZE ) {
+ $buf = $this->writeBuffer;
+ $flags = MSG_EOR;
+ } else {
+ $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE );
+ $flags = 0;
+ }
+ wfSuppressWarnings();
+ $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags );
+ wfRestoreWarnings();
+
+ if ( $bytesSent === false ) {
+ $error = socket_last_error( $socket );
+ if ( $error != self::EAGAIN && $error != self::EINTR ) {
+ $this->log( 'write error: ' . socket_strerror( $error ) );
+ $this->markDown();
+ }
+ return;
+ }
+
+ $this->writeBuffer = substr( $this->writeBuffer, $bytesSent );
+ }
+
+ /**
+ * Read some data. Call this when socket_select() indicates that the read buffer is non-empty.
+ */
+ public function doReads() {
+ $socket = $this->getSocket();
+ if ( !$socket ) {
+ return;
+ }
+
+ $buf = '';
+ wfSuppressWarnings();
+ $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 );
+ wfRestoreWarnings();
+ if ( $bytesRead === false ) {
+ $error = socket_last_error( $socket );
+ if ( $error != self::EAGAIN && $error != self::EINTR ) {
+ $this->log( 'read error: ' . socket_strerror( $error ) );
+ $this->markDown();
+ return;
+ }
+ } elseif ( $bytesRead === 0 ) {
+ // Assume EOF
+ $this->close();
+ return;
+ }
+
+ $this->readBuffer .= $buf;
+ while ( $this->socket && $this->processReadBuffer() === 'continue' );
+ }
+
+ protected function processReadBuffer() {
+ switch ( $this->readState ) {
+ case 'idle':
+ return 'done';
+ case 'status':
+ case 'header':
+ $lines = explode( "\r\n", $this->readBuffer, 2 );
+ if ( count( $lines ) < 2 ) {
+ return 'done';
+ }
+ if ( $this->readState == 'status' ) {
+ $this->processStatusLine( $lines[0] );
+ } else { // header
+ $this->processHeaderLine( $lines[0] );
+ }
+ $this->readBuffer = $lines[1];
+ return 'continue';
+ case 'body':
+ if ( $this->bodyRemaining !== null ) {
+ if ( $this->bodyRemaining > strlen( $this->readBuffer ) ) {
+ $this->bodyRemaining -= strlen( $this->readBuffer );
+ $this->readBuffer = '';
+ return 'done';
+ } else {
+ $this->readBuffer = substr( $this->readBuffer, $this->bodyRemaining );
+ $this->bodyRemaining = 0;
+ $this->nextRequest();
+ return 'continue';
+ }
+ } else {
+ // No content length, read all data to EOF
+ $this->readBuffer = '';
+ return 'done';
+ }
+ default:
+ throw new MWException( __METHOD__.': unexpected state' );
+ }
+ }
+
+ protected function processStatusLine( $line ) {
+ if ( !preg_match( '!^HTTP/(\d+)\.(\d+) (\d{3}) (.*)$!', $line, $m ) ) {
+ $this->log( 'invalid status line' );
+ $this->markDown();
+ return;
+ }
+ list( $all, $major, $minor, $status, $reason ) = $m;
+ $status = intval( $status );
+ if ( $status !== 200 && $status !== 404 ) {
+ $this->log( "unexpected status code: $status $reason" );
+ $this->markDown();
+ return;
+ }
+ $this->readState = 'header';
+ }
+
+ protected function processHeaderLine( $line ) {
+ if ( preg_match( '/^Content-Length: (\d+)$/i', $line, $m ) ) {
+ $this->bodyRemaining = intval( $m[1] );
+ } elseif ( $line === '' ) {
+ $this->readState = 'body';
+ }
+ }
+
+ protected function nextRequest() {
+ if ( $this->currentRequestIndex !== null ) {
+ unset( $this->requests[$this->currentRequestIndex] );
+ }
+ if ( count( $this->requests ) ) {
+ $this->readState = 'status';
+ $this->currentRequestIndex = key( $this->requests );
+ $this->writeBuffer = $this->requests[$this->currentRequestIndex];
+ } else {
+ $this->readState = 'idle';
+ $this->currentRequestIndex = null;
+ $this->writeBuffer = '';
+ }
+ $this->bodyRemaining = null;
+ }
+
+ protected function log( $msg ) {
+ wfDebugLog( 'squid', __CLASS__." ($this->host): $msg\n" );
+ }
+}
+
+class SquidPurgeClientPool {
+ var $clients = array();
+ var $timeout = 5;
+
+ function __construct( $options = array() ) {
+ if ( isset( $options['timeout'] ) ) {
+ $this->timeout = $options['timeout'];
+ }
+ }
+
+ public function addClient( $client ) {
+ $this->clients[] = $client;
+ }
+
+ public function run() {
+ $done = false;
+ $startTime = microtime( true );
+ while ( !$done ) {
+ $readSockets = $writeSockets = array();
+ foreach ( $this->clients as $clientIndex => $client ) {
+ $sockets = $client->getReadSocketsForSelect();
+ foreach ( $sockets as $i => $socket ) {
+ $readSockets["$clientIndex/$i"] = $socket;
+ }
+ $sockets = $client->getWriteSocketsForSelect();
+ foreach ( $sockets as $i => $socket ) {
+ $writeSockets["$clientIndex/$i"] = $socket;
+ }
+ }
+ if ( !count( $readSockets ) && !count( $writeSockets ) ) {
+ break;
+ }
+ $exceptSockets = null;
+ $timeout = min( $startTime + $this->timeout - microtime( true ), 1 );
+ wfSuppressWarnings();
+ $numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout );
+ wfRestoreWarnings();
+ if ( $numReady === false ) {
+ wfDebugLog( 'squid', __METHOD__.': Error in stream_select: ' .
+ socket_strerror( socket_last_error() ) . "\n" );
+ break;
+ }
+ // Check for timeout, use 1% tolerance since we aimed at having socket_select()
+ // exit at precisely the overall timeout
+ if ( microtime( true ) - $startTime > $this->timeout * 0.99 ) {
+ wfDebugLog( 'squid', __CLASS__.": timeout ({$this->timeout}s)\n" );
+ break;
+ } elseif ( !$numReady ) {
+ continue;
+ }
+
+ foreach ( $readSockets as $key => $socket ) {
+ list( $clientIndex, $i ) = explode( '/', $key );
+ $client = $this->clients[$clientIndex];
+ $client->doReads();
+ }
+ foreach ( $writeSockets as $key => $socket ) {
+ list( $clientIndex, $i ) = explode( '/', $key );
+ $client = $this->clients[$clientIndex];
+ $client->doWrites();
+ }
+
+ $done = true;
+ foreach ( $this->clients as $client ) {
+ if ( !$client->isIdle() ) {
+ $done = false;
+ }
+ }
+ }
+ foreach ( $this->clients as $client ) {
+ $client->close();
+ }
+ }
+}
diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php
index b1f01924..66517719 100644
--- a/includes/SquidUpdate.php
+++ b/includes/SquidUpdate.php
@@ -26,8 +26,7 @@ class SquidUpdate {
}
static function newFromLinksTo( &$title ) {
- $fname = 'SquidUpdate::newFromLinksTo';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
# Get a list of URLs linking to this page
$dbr = wfGetDB( DB_SLAVE );
@@ -37,7 +36,7 @@ class SquidUpdate {
'pl_namespace' => $title->getNamespace(),
'pl_title' => $title->getDBkey(),
'pl_from=page_id' ),
- $fname );
+ __METHOD__ );
$blurlArr = $title->getSquidURLs();
if ( $dbr->numRows( $res ) <= $this->mMaxTitles ) {
while ( $BL = $dbr->fetchObject ( $res ) )
@@ -48,7 +47,7 @@ class SquidUpdate {
}
$dbr->freeResult ( $res ) ;
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return new SquidUpdate( $blurlArr );
}
@@ -89,7 +88,7 @@ class SquidUpdate {
return;
}*/
- if( empty( $urlArr ) ) {
+ if( !$urlArr ) {
return;
}
@@ -97,115 +96,34 @@ class SquidUpdate {
return SquidUpdate::HTCPPurge( $urlArr );
}
- $fname = 'SquidUpdate::purge';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
- $maxsocketspersquid = 8; // socket cap per Squid
- $urlspersocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while
- $firsturl = SquidUpdate::expand( $urlArr[0] );
- unset($urlArr[0]);
- $urlArr = array_values($urlArr);
- $sockspersq = max(ceil(count($urlArr) / $urlspersocket ),1);
- if ($sockspersq == 1) {
- /* the most common case */
- $urlspersocket = count($urlArr);
- } else if ($sockspersq > $maxsocketspersquid ) {
- $urlspersocket = ceil(count($urlArr) / $maxsocketspersquid);
- $sockspersq = $maxsocketspersquid;
+ $maxSocketsPerSquid = 8; // socket cap per Squid
+ $urlsPerSocket = 400; // 400 seems to be a good tradeoff, opening a socket takes a while
+ $socketsPerSquid = ceil( count( $urlArr ) / $urlsPerSocket );
+ if ( $socketsPerSquid > $maxSocketsPerSquid ) {
+ $socketsPerSquid = $maxSocketsPerSquid;
}
- $totalsockets = count($wgSquidServers) * $sockspersq;
- $sockets = Array();
- /* this sets up the sockets and tests the first socket for each server. */
- for ($ss=0;$ss < count($wgSquidServers);$ss++) {
- $failed = false;
- $so = 0;
- while ($so < $sockspersq && !$failed) {
- if ($so == 0) {
- /* first socket for this server, do the tests */
- @list($server, $port) = explode(':', $wgSquidServers[$ss]);
- if(!isset($port)) $port = 80;
- #$this->debug("Opening socket to $server:$port");
- $error = $errstr = false;
- $socket = @fsockopen($server, $port, $error, $errstr, 3);
- #$this->debug("\n");
- if (!$socket) {
- $failed = true;
- $totalsockets -= $sockspersq;
- } else {
- $msg = 'PURGE ' . $firsturl . " HTTP/1.0\r\n".
- "Connection: Keep-Alive\r\n\r\n";
- #$this->debug($msg);
- @fputs($socket,$msg);
- #$this->debug("...");
- $res = @fread($socket,512);
- #$this->debug("\n");
- /* Squid only returns http headers with 200 or 404 status,
- if there's more returned something's wrong */
- if (strlen($res) > 250) {
- fclose($socket);
- $failed = true;
- $totalsockets -= $sockspersq;
- } else {
- @stream_set_blocking($socket,false);
- $sockets[] = $socket;
- }
- }
- } else {
- /* open the remaining sockets for this server */
- list($server, $port) = explode(':', $wgSquidServers[$ss]);
- if(!isset($port)) $port = 80;
- $socket = @fsockopen($server, $port, $error, $errstr, 2);
- @stream_set_blocking($socket,false);
- $sockets[] = $socket;
+ $pool = new SquidPurgeClientPool;
+ $chunks = array_chunk( $urlArr, ceil( count( $urlArr ) / $socketsPerSquid ) );
+ foreach ( $wgSquidServers as $server ) {
+ foreach ( $chunks as $chunk ) {
+ $client = new SquidPurgeClient( $server );
+ foreach ( $chunk as $url ) {
+ $client->queuePurge( $url );
}
- $so++;
+ $pool->addClient( $client );
}
}
+ $pool->run();
- if ($urlspersocket > 0) {
- /* now do the heavy lifting. The fread() relies on Squid returning only the headers */
- for ($r=0;$r < $urlspersocket;$r++) {
- for ($s=0;$s < $totalsockets;$s++) {
- if($r != 0) {
- $res = '';
- $esc = 0;
- while (strlen($res) < 100 && $esc < 200 ) {
- $res .= @fread($sockets[$s],512);
- $esc++;
- usleep(20);
- }
- }
- $urindex = $r + $urlspersocket * ($s - $sockspersq * floor($s / $sockspersq));
- $url = SquidUpdate::expand( $urlArr[$urindex] );
- $msg = 'PURGE ' . $url . " HTTP/1.0\r\n".
- "Connection: Keep-Alive\r\n\r\n";
- #$this->debug($msg);
- @fputs($sockets[$s],$msg);
- #$this->debug("\n");
- }
- }
- }
- #$this->debug("Reading response...");
- foreach ($sockets as $socket) {
- $res = '';
- $esc = 0;
- while (strlen($res) < 100 && $esc < 200 ) {
- $res .= @fread($socket,1024);
- $esc++;
- usleep(20);
- }
-
- @fclose($socket);
- }
- #$this->debug("\n");
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
}
static function HTCPPurge( $urlArr ) {
global $wgHTCPMulticastAddress, $wgHTCPMulticastTTL, $wgHTCPPort;
- $fname = 'SquidUpdate::HTCPPurge';
- wfProfileIn( $fname );
+ wfProfileIn( __METHOD__ );
$htcpOpCLR = 4; // HTCP CLR
@@ -217,7 +135,7 @@ class SquidUpdate {
}
// pfsockopen doesn't work because we need set_sock_opt
- $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
+ $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
if ( $conn ) {
// Set socket options
socket_set_option( $conn, IPPROTO_IP, IP_MULTICAST_LOOP, 0 );
@@ -257,16 +175,9 @@ class SquidUpdate {
}
} else {
$errstr = socket_strerror( socket_last_error() );
- wfDebug( "SquidUpdate::HTCPPurge(): Error opening UDP socket: $errstr\n" );
- }
- wfProfileOut( $fname );
- }
-
- function debug( $text ) {
- global $wgDebugSquid;
- if ( $wgDebugSquid ) {
- wfDebug( $text );
+ wfDebug( __METHOD__ . "(): Error opening UDP socket: $errstr\n" );
}
+ wfProfileOut( __METHOD__ );
}
/**
diff --git a/includes/Status.php b/includes/Status.php
index 185ea6e5..a07a4b81 100644
--- a/includes/Status.php
+++ b/includes/Status.php
@@ -84,6 +84,13 @@ class Status {
$this->ok = false;
}
+ /**
+ * Sanitize the callback parameter on wakeup, to avoid arbitrary execution.
+ */
+ function __wakeup() {
+ $this->cleanCallback = false;
+ }
+
protected function cleanParams( $params ) {
if ( !$this->cleanCallback ) {
return $params;
@@ -152,7 +159,7 @@ class Status {
if ( $longContext ) {
$s = wfMsgNoTrans( $longContext, $s );
} elseif ( $shortContext ) {
- $s = wfMsgNoTrans( $shortContext, "\n* $s\n" );
+ $s = wfMsgNoTrans( $shortContext, "\n$s\n" );
}
}
return $s;
@@ -170,12 +177,15 @@ class Status {
$this->successCount += $other->successCount;
$this->failCount += $other->failCount;
}
-
+
function getErrorsArray() {
$result = array();
foreach ( $this->errors as $error ) {
if ( $error['type'] == 'error' )
- $result[] = $error['message'];
+ if( $error['params'] )
+ $result[] = array_merge( array( $error['message'] ), $error['params'] );
+ else
+ $result[] = $error['message'];
}
return $result;
}
diff --git a/includes/StreamFile.php b/includes/StreamFile.php
index bdd2a2e5..6db66ba8 100644
--- a/includes/StreamFile.php
+++ b/includes/StreamFile.php
@@ -92,13 +92,12 @@ function wfGetType( $filename, $safe = true ) {
if ( $safe ) {
global $wgFileBlacklist, $wgCheckFileExtensions, $wgStrictFileExtensions,
$wgFileExtensions, $wgVerifyMimeType, $wgMimeTypeBlacklist, $wgRequest;
- $form = new UploadForm( $wgRequest );
- list( $partName, $extList ) = $form->splitExtensions( $filename );
- if ( $form->checkFileExtensionList( $extList, $wgFileBlacklist ) ) {
+ list( $partName, $extList ) = UploadBase::splitExtensions( $filename );
+ if ( UploadBase::checkFileExtensionList( $extList, $wgFileBlacklist ) ) {
return 'unknown/unknown';
}
if ( $wgCheckFileExtensions && $wgStrictFileExtensions
- && !$form->checkFileExtensionList( $extList, $wgFileExtensions ) )
+ && !UploadBase::checkFileExtensionList( $extList, $wgFileExtensions ) )
{
return 'unknown/unknown';
}
diff --git a/includes/StubObject.php b/includes/StubObject.php
index f1847a39..c8731fff 100644
--- a/includes/StubObject.php
+++ b/includes/StubObject.php
@@ -88,6 +88,10 @@ class StubObject {
*/
function _unstub( $name = '_unstub', $level = 2 ) {
static $recursionLevel = 0;
+
+ if ( !($GLOBALS[$this->mGlobal] instanceof StubObject) )
+ return $GLOBALS[$this->mGlobal]; // already unstubbed.
+
if ( get_class( $GLOBALS[$this->mGlobal] ) != $this->mClass ) {
$fname = __METHOD__.'-'.$this->mGlobal;
wfProfileIn( $fname );
@@ -96,7 +100,7 @@ class StubObject {
throw new MWException( "Unstub loop detected on call of \${$this->mGlobal}->$name from $caller\n" );
}
wfDebug( "Unstubbing \${$this->mGlobal} on call of \${$this->mGlobal}::$name from $caller\n" );
- $GLOBALS[$this->mGlobal] = $this->_newObject();
+ $obj = $GLOBALS[$this->mGlobal] = $this->_newObject();
--$recursionLevel;
wfProfileOut( $fname );
}
@@ -144,14 +148,8 @@ class StubUserLang extends StubObject {
function _newObject() {
global $wgContLanguageCode, $wgRequest, $wgUser, $wgContLang;
$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() && in_array($code, $wgContLang->getVariants()) ){
- $variant = $wgContLang->getPreferredVariant();
- if( $variant != $wgContLanguageCode )
- $code = $variant;
- }
+ // BCP 47 - letter case MUST NOT carry meaning
+ $code = strtolower( $code );
# Validate $code
if( empty( $code ) || !preg_match( '/^[a-z-]+$/', $code ) || ( $code === 'qqq' ) ) {
diff --git a/includes/Title.php b/includes/Title.php
index f6c0d5de..8d7275ff 100644
--- a/includes/Title.php
+++ b/includes/Title.php
@@ -10,12 +10,6 @@ if ( !class_exists( 'UtfNormal' ) ) {
define ( 'GAID_FOR_UPDATE', 1 );
-
-/**
- * Constants for pr_cascade bitfield
- */
-define( 'CASCADE', 1 );
-
/**
* Represents a title within MediaWiki.
* Optionally may contain an interwiki designation or namespace.
@@ -44,32 +38,32 @@ class Title {
*/
//@{
- var $mTextform = ''; ///< Text form (spaces not underscores) of the main part
- var $mUrlform = ''; ///< URL-encoded form of the main part
- var $mDbkeyform = ''; ///< Main part with underscores
+ var $mTextform = ''; ///< Text form (spaces not underscores) of the main part
+ var $mUrlform = ''; ///< URL-encoded form of the main part
+ var $mDbkeyform = ''; ///< Main part with underscores
var $mUserCaseDBKey; ///< DB key with the initial letter in the case specified by the user
var $mNamespace = NS_MAIN; ///< Namespace index, i.e. one of the NS_xxxx constants
- var $mInterwiki = ''; ///< Interwiki prefix (or null string)
- var $mFragment; ///< Title fragment (i.e. the bit after the #)
+ var $mInterwiki = ''; ///< Interwiki prefix (or null string)
+ var $mFragment; ///< Title fragment (i.e. the bit after the #)
var $mArticleID = -1; ///< Article ID, fetched from the link cache on demand
var $mLatestID = false; ///< ID of most recent revision
var $mRestrictions = array(); ///< Array of groups allowed to edit this article
var $mOldRestrictions = false;
- var $mCascadeRestriction; ///< Cascade restrictions on this page to included templates and images?
- var $mRestrictionsExpiry = array(); ///< When do the restrictions on this page expire?
- var $mHasCascadingRestrictions; ///< Are cascading restrictions in effect on this page?
- var $mCascadeSources; ///< Where are the cascading restrictions coming from on this page?
+ var $mCascadeRestriction; ///< Cascade restrictions on this page to included templates and images?
+ var $mRestrictionsExpiry = array(); ///< When do the restrictions on this page expire?
+ var $mHasCascadingRestrictions; ///< Are cascading restrictions in effect on this page?
+ var $mCascadeSources; ///< Where are the cascading restrictions coming from on this page?
var $mRestrictionsLoaded = false; ///< Boolean for initialisation on demand
- var $mPrefixedText; ///< Text form including namespace/interwiki, initialised on demand
+ var $mPrefixedText; ///< Text form including namespace/interwiki, initialised on demand
# Don't change the following default, NS_MAIN is hardcoded in several
# places. See bug 696.
var $mDefaultNamespace = NS_MAIN; ///< Namespace index when there is no namespace
- # Zero except in {{transclusion}} tags
- var $mWatched = null; ///< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
+ # Zero except in {{transclusion}} tags
+ var $mWatched = null; ///< Is $wgUser watching this page? null if unfilled, accessed through userIsWatching()
var $mLength = -1; ///< The page length, 0 for special pages
var $mRedirect = null; ///< Is the article at this title a redirect?
var $mNotificationTimestamp = array(); ///< Associative array of user ID -> timestamp/false
- var $mBacklinkCache = null; ///< Cache of links to this title
+ var $mBacklinkCache = null; ///< Cache of links to this title
//@}
@@ -92,7 +86,7 @@ class Title {
if( $t->secureAndSplit() )
return $t;
else
- return NULL;
+ return null;
}
/**
@@ -146,12 +140,20 @@ class Title {
}
return $t;
} else {
- $ret = NULL;
+ $ret = null;
return $ret;
}
}
/**
+ * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
+ *
+ * Example of wrong and broken code:
+ * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
+ *
+ * Example of right code:
+ * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
+ *
* Create a new Title from URL-encoded text. Ensures that
* the given title's length does not exceed the maximum.
* @param $url \type{\string} the title, as might be taken from a URL
@@ -172,29 +174,24 @@ class Title {
if( $t->secureAndSplit() ) {
return $t;
} else {
- return NULL;
+ return null;
}
}
/**
* Create a new Title from an article ID
*
- * @todo This is inefficiently implemented, the page row is requested
- * but not used for anything else
- *
* @param $id \type{\int} the page_id corresponding to the Title to create
* @param $flags \type{\int} use GAID_FOR_UPDATE to use master
* @return \type{Title} the new object, or NULL on an error
*/
public static function newFromID( $id, $flags = 0 ) {
- $fname = 'Title::newFromID';
$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 );
+ $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ );
+ if( $row !== false ) {
+ $title = Title::newFromRow( $row );
} else {
- $title = NULL;
+ $title = null;
}
return $title;
}
@@ -229,7 +226,7 @@ class 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->mRedirect = isset($row->page_is_redirect) ? (bool)$row->page_is_redirect : null;
$t->mLatestID = isset($row->page_latest) ? $row->page_latest : false;
return $t;
@@ -275,9 +272,9 @@ class Title {
if( $t->secureAndSplit() ) {
return $t;
} else {
- return NULL;
+ return null;
}
- }
+ }
/**
* Create a new Title for the Main Page
@@ -304,7 +301,7 @@ class Title {
public static function newFromRedirect( $text ) {
return self::newFromRedirectInternal( $text );
}
-
+
/**
* Extract a redirect destination from a string and return the
* Title, or null if the text doesn't contain a valid redirect
@@ -318,7 +315,7 @@ class Title {
$titles = self::newFromRedirectArray( $text );
return $titles ? array_pop( $titles ) : null;
}
-
+
/**
* Extract a redirect destination from a string and return an
* array of Titles, or null if the text doesn't contain a valid redirect
@@ -357,7 +354,7 @@ class Title {
}
return $titles;
}
-
+
/**
* Really extract the redirect destination
* Do not call this function directly, use one of the newFromRedirect* functions above
@@ -401,16 +398,16 @@ class Title {
* Get the prefixed DB key associated with an ID
* @param $id \type{\int} the page_id of the article
* @return \type{Title} an object representing the article, or NULL
- * if no such article was found
+ * if no such article was found
*/
public static function nameOf( $id ) {
$dbr = wfGetDB( DB_SLAVE );
$s = $dbr->selectRow( 'page',
array( 'page_namespace','page_title' ),
- array( 'page_id' => $id ),
+ array( 'page_id' => $id ),
__METHOD__ );
- if ( $s === false ) { return NULL; }
+ if ( $s === false ) { return null; }
$n = self::makeName( $s->page_namespace, $s->page_title );
return $n;
@@ -432,13 +429,13 @@ class Title {
* @param $ns \type{\int} a namespace index
* @param $title \type{\string} text-form main part
* @return \type{\string} a stripped-down title string ready for the
- * search index
+ * search index
*/
public static function indexTitle( $ns, $title ) {
global $wgContLang;
$lc = SearchEngine::legalSearchChars() . '&#;';
- $t = $wgContLang->stripForSearch( $title );
+ $t = $wgContLang->normalizeForSearch( $title );
$t = preg_replace( "/[^{$lc}]+/", ' ', $t );
$t = $wgContLang->lc( $t );
@@ -454,7 +451,7 @@ class Title {
return trim( $t );
}
- /*
+ /**
* Make a prefixed DB key from a DB key and a namespace index
* @param $ns \type{\int} numerical representation of the namespace
* @param $title \type{\string} the DB key form the title
@@ -473,18 +470,6 @@ class Title {
}
/**
- * Returns the URL associated with an interwiki prefix
- * @param $key \type{\string} the interwiki prefix (e.g. "MeatBall")
- * @return \type{\string} the associated URL, containing "$1",
- * which should be replaced by an article title
- * @static (arguably)
- * @deprecated See Interwiki class
- */
- public function getInterwikiLink( $key ) {
- return Interwiki::fetch( $key )->getURL( );
- }
-
- /**
* Determine whether the object refers to a page within
* this project.
*
@@ -508,7 +493,7 @@ class Title {
public function isTrans() {
if ($this->mInterwiki == '')
return false;
-
+
return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
}
@@ -516,13 +501,11 @@ class Title {
* Escape a text fragment, say from a link, for a URL
*/
static function escapeFragmentForURL( $fragment ) {
- global $wgEnforceHtmlIds;
# Note that we don't urlencode the fragment. urlencoded Unicode
# fragments appear not to work in IE (at least up to 7) or in at least
# one version of Opera 9.x. The W3C validator, for one, doesn't seem
# to care if they aren't encoded.
- return Sanitizer::escapeId( $fragment,
- $wgEnforceHtmlIds ? 'noninitial' : 'xml' );
+ return Sanitizer::escapeId( $fragment, 'noninitial' );
}
#----------------------------------------------------------------------------
@@ -555,17 +538,17 @@ class Title {
* @return \type{\string} Namespace text
*/
public function getNsText() {
- global $wgContLang, $wgCanonicalNamespaceNames;
+ global $wgContLang;
- if ( '' != $this->mInterwiki ) {
+ if ( $this->mInterwiki != '' ) {
// This probably shouldn't even happen. ohh man, oh yuck.
// But for interwiki transclusion it sometimes does.
// Shit. Shit shit shit.
//
// Use the canonical namespaces if possible to try to
// resolve a foreign namespace.
- if( isset( $wgCanonicalNamespaceNames[$this->mNamespace] ) ) {
- return $wgCanonicalNamespaceNames[$this->mNamespace];
+ if( MWNamespace::exists( $this->mNamespace ) ) {
+ return MWNamespace::getCanonicalName( $this->mNamespace );
}
}
return $wgContLang->getNsText( $this->mNamespace );
@@ -630,7 +613,7 @@ class Title {
/**
* Get title for search index
* @return \type{\string} a stripped-down title string ready for the
- * search index
+ * search index
*/
public function getIndexTitle() {
return Title::indexTitle( $this->mNamespace, $this->mTextform );
@@ -639,7 +622,7 @@ class Title {
/**
* Get the prefixed database key form
* @return \type{\string} the prefixed title, with underscores and
- * any interwiki and namespace prefixes
+ * any interwiki and namespace prefixes
*/
public function getPrefixedDBkey() {
$s = $this->prefix( $this->mDbkeyform );
@@ -665,11 +648,11 @@ class Title {
* Get the prefixed title with spaces, plus any fragment
* (part beginning with '#')
* @return \type{\string} the prefixed title, with spaces and
- * the fragment, including '#'
+ * the fragment, including '#'
*/
public function getFullText() {
$text = $this->getPrefixedText();
- if( '' != $this->mFragment ) {
+ if( $this->mFragment != '' ) {
$text .= '#' . $this->mFragment;
}
return $text;
@@ -742,7 +725,7 @@ class Title {
$interwiki = Interwiki::fetch( $this->mInterwiki );
if ( !$interwiki ) {
- $url = $this->getLocalUrl( $query, $variant );
+ $url = $this->getLocalURL( $query, $variant );
// Ugly quick hack to avoid duplicate prefixes (bug 4571 etc)
// Correct fix would be to move the prepending elsewhere.
@@ -753,7 +736,7 @@ class Title {
$baseUrl = $interwiki->getURL( );
$namespace = wfUrlencode( $this->getNsText() );
- if ( '' != $namespace ) {
+ if ( $namespace != '' ) {
# Can this actually happen? Interwikis shouldn't be parsed.
# Yes! It can in interwiki transclusion. But... it probably shouldn't.
$namespace .= ':';
@@ -773,7 +756,7 @@ class Title {
* Get a URL with no fragment or server name. If this page is generated
* with action=render, $wgServer is prepended.
* @param mixed $query an optional query string; if not specified,
- * $wgArticlePath will be used. Can be specified as an associative array
+ * $wgArticlePath will be used. Can be specified as an associative array
* as well, e.g., array( 'action' => 'edit' ) (keys and values will be
* URL-escaped).
* @param $variant \type{\string} language variant of url (for sr, zh..)
@@ -859,6 +842,9 @@ class Title {
* there's a fragment but the prefixed text is empty, we just return a link
* to the fragment.
*
+ * The result obviously should not be URL-escaped, but does need to be
+ * HTML-escaped if it's being output in HTML.
+ *
* @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the
* query string. Keys and values will be escaped.
* @param $variant \type{\string} Language variant of URL (for sr, zh..). Ignored
@@ -868,11 +854,6 @@ class Title {
*/
public function getLinkUrl( $query = array(), $variant = false ) {
wfProfileIn( __METHOD__ );
- if( !is_array( $query ) ) {
- wfProfileOut( __METHOD__ );
- throw new MWException( 'Title::getLinkUrl passed a non-array for '.
- '$query' );
- }
if( $this->isExternal() ) {
$ret = $this->getFullURL( $query );
} elseif( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
@@ -924,10 +905,10 @@ class Title {
/**
* Get the edit URL for this Title
* @return \type{\string} the URL, or a null string if this is an
- * interwiki link
+ * interwiki link
*/
public function getEditURL() {
- if ( '' != $this->mInterwiki ) { return ''; }
+ if ( $this->mInterwiki != '' ) { return ''; }
$s = $this->getLocalURL( 'action=edit' );
return $s;
@@ -946,7 +927,7 @@ class Title {
* Is this Title interwiki?
* @return \type{\bool}
*/
- public function isExternal() { return ( '' != $this->mInterwiki ); }
+ public function isExternal() { return ( $this->mInterwiki != '' ); }
/**
* Is this page "semi-protected" - the *only* protection is autoconfirm?
@@ -976,18 +957,20 @@ class Title {
/**
* Does the title correspond to a protected article?
* @param $what \type{\string} the action the page is protected from,
- * by default checks move and edit
+ * by default checks all actions.
* @return \type{\bool}
*/
public function isProtected( $action = '' ) {
- global $wgRestrictionLevels, $wgRestrictionTypes;
+ global $wgRestrictionLevels;
+
+ $restrictionTypes = $this->getRestrictionTypes();
# Special pages have inherent protection
if( $this->getNamespace() == NS_SPECIAL )
return true;
# Check regular protection levels
- foreach( $wgRestrictionTypes as $type ){
+ foreach( $restrictionTypes as $type ){
if( $action == $type || $action == '' ) {
$r = $this->getRestrictions( $type );
foreach( $wgRestrictionLevels as $level ) {
@@ -1002,6 +985,19 @@ class Title {
}
/**
+ * Is this a conversion table for the LanguageConverter?
+ * @return \type{\bool}
+ */
+ public function isConversionTable() {
+ if($this->getNamespace() == NS_MEDIAWIKI
+ && strpos( $this->getText(), 'Conversiontable' ) !== false ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
* Is $wgUser watching this page?
* @return \type{\bool}
*/
@@ -1020,7 +1016,8 @@ class Title {
/**
* Can $wgUser perform $action on this page?
- * This skips potentially expensive cascading permission checks.
+ * This skips potentially expensive cascading permission checks
+ * as well as avoids expensive error formatting
*
* Suitable for use for nonessential UI controls in common cases, but
* _not_ for functional access control.
@@ -1029,7 +1026,7 @@ class Title {
*
* @param $action \type{\string} action that permission needs to be checked for
* @return \type{\bool}
- */
+ */
public function quickUserCan( $action ) {
return $this->userCan( $action, false );
}
@@ -1056,7 +1053,7 @@ class Title {
* @param $action \type{\string} action that permission needs to be checked for
* @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries.
* @return \type{\bool}
- */
+ */
public function userCan( $action, $doExpensiveQueries = true ) {
global $wgUser;
return ($this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array());
@@ -1136,15 +1133,15 @@ class Title {
$intended = $user->mBlock->mAddress;
- $errors[] = array( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name,
+ $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]);
}
@@ -1177,15 +1174,29 @@ class Title {
// Show user page-specific message only if the user can move other pages
$errors[] = array( 'cant-move-user-page' );
}
-
+
// Check if user is allowed to move files if it's a file
if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
$errors[] = array( 'movenotallowedfile' );
}
-
+
if( !$user->isAllowed( 'move' ) ) {
// User can't move anything
- $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+ global $wgGroupPermissions;
+ $userCanMove = false;
+ if ( isset( $wgGroupPermissions['user']['move'] ) ) {
+ $userCanMove = $wgGroupPermissions['user']['move'];
+ }
+ $autoconfirmedCanMove = false;
+ if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
+ $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
+ }
+ if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
+ // custom message if logged-in users without any special rights can move
+ $errors[] = array ( 'movenologintext' );
+ } else {
+ $errors[] = array ('movenotallowed');
+ }
}
} elseif ( $action == 'create' ) {
if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
@@ -1196,7 +1207,7 @@ class Title {
} elseif( $action == 'move-target' ) {
if( !$user->isAllowed( 'move' ) ) {
// User can't move anything
- $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed');
+ $errors[] = array ('movenotallowed');
} elseif( !$user->isAllowed( 'move-rootuserpages' )
&& $this->getNamespace() == NS_USER && !$this->isSubpage() )
{
@@ -1205,8 +1216,14 @@ class Title {
}
} elseif( !$user->isAllowed( $action ) ) {
$return = null;
- $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
- User::getGroupsWithPermission( $action ) );
+
+ // We avoid expensive display logic for quickUserCan's and such
+ $groups = false;
+ if (!$short) {
+ $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
+ User::getGroupsWithPermission( $action ) );
+ }
+
if( $groups ) {
$return = array( 'badaccess-groups',
array( implode( ', ', $groups ), count( $groups ) ) );
@@ -1259,7 +1276,7 @@ class Title {
wfProfileOut( __METHOD__ );
return $errors;
}
-
+
# Only 'createaccount' and 'execute' can be performed on
# special pages, which don't actually exist in the DB.
$specialOKActions = array( 'createaccount', 'execute' );
@@ -1277,8 +1294,16 @@ class Title {
# Protect css/js subpages of user pages
# XXX: this might be better using restrictions
- # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working
- if( $this->isCssJsSubpage() && !$user->isAllowed('editusercssjs')
+ # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssSubpage()
+ # and $this->userCanEditJsSubpage() from working
+ # XXX: right 'editusercssjs' is deprecated, for backward compatibility only
+ if( $this->isCssSubpage() && !( $user->isAllowed('editusercssjs') || $user->isAllowed('editusercss') )
+ && $action != 'patrol'
+ && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) )
+ {
+ $errors[] = array('customcssjsprotected');
+ } else if( $this->isJsSubpage() && !( $user->isAllowed('editusercssjs') || $user->isAllowed('edituserjs') )
+ && $action != 'patrol'
&& !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) )
{
$errors[] = array('customcssjsprotected');
@@ -1291,7 +1316,7 @@ class Title {
if( $right == 'sysop' ) {
$right = 'protect';
}
- if( '' != $right && !$user->isAllowed( $right ) ) {
+ if( $right != '' && !$user->isAllowed( $right ) ) {
// Users with 'editprotected' permission can edit protected pages
if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) {
// Users with 'editprotected' permission cannot edit protected pages
@@ -1309,7 +1334,7 @@ class Title {
wfProfileOut( __METHOD__ );
return $errors;
}
-
+
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
@@ -1323,7 +1348,7 @@ class Title {
if( $cascadingSources > 0 && isset($restrictions[$action]) ) {
foreach( $restrictions[$action] as $right ) {
$right = ( $right == 'sysop' ) ? 'protect' : $right;
- if( '' != $right && !$user->isAllowed( $right ) ) {
+ if( $right != '' && !$user->isAllowed( $right ) ) {
$pages = '';
foreach( $cascadingSources as $page )
$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
@@ -1388,6 +1413,11 @@ class Title {
return false;
}
+ // Can't protect pages that exist.
+ if ($this->exists()) {
+ return false;
+ }
+
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'protected_titles', '*',
array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
@@ -1423,32 +1453,40 @@ class Title {
$expiry_description = '';
if ( $encodedExpiry != 'infinity' ) {
- $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) , $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')';
+ $expiry_description = ' (' . wfMsgForContent( 'protect-expiring',$wgContLang->timeanddate( $expiry ),
+ $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')';
}
else {
$expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ).')';
}
-
+
# Update protection table
if ($create_perm != '' ) {
$dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')),
- array( 'pt_namespace' => $namespace, 'pt_title' => $title
- , 'pt_create_perm' => $create_perm
- , 'pt_timestamp' => Block::encodeExpiry(wfTimestampNow(), $dbw)
- , 'pt_expiry' => $encodedExpiry
- , 'pt_user' => $wgUser->getId(), 'pt_reason' => $reason ), __METHOD__ );
+ array(
+ 'pt_namespace' => $namespace,
+ 'pt_title' => $title,
+ 'pt_create_perm' => $create_perm,
+ 'pt_timestamp' => Block::encodeExpiry(wfTimestampNow(), $dbw),
+ 'pt_expiry' => $encodedExpiry,
+ 'pt_user' => $wgUser->getId(),
+ 'pt_reason' => $reason,
+ ), __METHOD__
+ );
} else {
$dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace,
'pt_title' => $title ), __METHOD__ );
}
# Update the protection log
- $log = new LogPage( 'protect' );
+ if( $dbw->affectedRows() ) {
+ $log = new LogPage( 'protect' );
- if( $create_perm ) {
- $params = array("[create=$create_perm] $expiry_description",'');
- $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason ), $params );
- } else {
- $log->addEntry( 'unprotect', $this, $reason );
+ if( $create_perm ) {
+ $params = array("[create=$create_perm] $expiry_description",'');
+ $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params );
+ } else {
+ $log->addEntry( 'unprotect', $this, $reason );
+ }
}
return true;
@@ -1461,38 +1499,11 @@ class Title {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete( 'protected_titles',
- array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
+ array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
__METHOD__ );
}
/**
- * Can $wgUser edit this page?
- * @return \type{\bool} TRUE or FALSE
- * @deprecated use userCan('edit')
- */
- public function userCanEdit( $doExpensiveQueries = true ) {
- return $this->userCan( 'edit', $doExpensiveQueries );
- }
-
- /**
- * Can $wgUser create this page?
- * @return \type{\bool} TRUE or FALSE
- * @deprecated use userCan('create')
- */
- public function userCanCreate( $doExpensiveQueries = true ) {
- return $this->userCan( 'create', $doExpensiveQueries );
- }
-
- /**
- * Can $wgUser move this page?
- * @return \type{\bool} TRUE or FALSE
- * @deprecated use userCan('move')
- */
- public function userCanMove( $doExpensiveQueries = true ) {
- return $this->userCan( 'move', $doExpensiveQueries );
- }
-
- /**
* Would anybody with sufficient privileges be able to move this page?
* Some pages just aren't movable.
*
@@ -1510,6 +1521,32 @@ class Title {
public function userCanRead() {
global $wgUser, $wgGroupPermissions;
+ static $useShortcut = null;
+
+ # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
+ if( is_null( $useShortcut ) ) {
+ global $wgRevokePermissions;
+ $useShortcut = true;
+ if( empty( $wgGroupPermissions['*']['read'] ) ) {
+ # Not a public wiki, so no shortcut
+ $useShortcut = false;
+ } elseif( !empty( $wgRevokePermissions ) ) {
+ /*
+ * Iterate through each group with permissions being revoked (key not included since we don't care
+ * what the group name is), then check if the read permission is being revoked. If it is, then
+ * we don't use the shortcut below since the user might not be able to read, even though anon
+ * reading is allowed.
+ */
+ foreach( $wgRevokePermissions as $perms ) {
+ if( !empty( $perms['read'] ) ) {
+ # We might be removing the read right from the user, so no shortcut
+ $useShortcut = false;
+ break;
+ }
+ }
+ }
+ }
+
$result = null;
wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) );
if ( $result !== null ) {
@@ -1517,7 +1554,7 @@ class Title {
}
# Shortcut for public wikis, allows skipping quite a bit of code
- if ( !empty( $wgGroupPermissions['*']['read'] ) )
+ if ( $useShortcut )
return true;
if( $wgUser->isAllowed( 'read' ) ) {
@@ -1620,7 +1657,7 @@ class Title {
return $this->mHasSubpages = (bool)$subpages->count();
return $this->mHasSubpages = false;
}
-
+
/**
* Get all subpages of this page.
* @param $limit Maximum number of subpages to fetch; -1 for no limit
@@ -1633,8 +1670,7 @@ class Title {
$dbr = wfGetDB( DB_SLAVE );
$conds['page_namespace'] = $this->getNamespace();
- $conds[] = 'page_title LIKE ' . $dbr->addQuotes(
- $dbr->escapeLike( $this->getDBkey() ) . '/%' );
+ $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
$options = array();
if( $limit > -1 )
$options['LIMIT'] = $limit;
@@ -1702,15 +1738,28 @@ class Title {
return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.js$/", $this->mTextform ) );
}
/**
- * Protect css/js subpages of user pages: can $wgUser edit
+ * Protect css subpages of user pages: can $wgUser edit
+ * this page?
+ *
+ * @return \type{\bool} TRUE or FALSE
+ * @todo XXX: this might be better using restrictions
+ */
+ public function userCanEditCssSubpage() {
+ global $wgUser;
+ return ( ( $wgUser->isAllowed('editusercssjs') && $wgUser->isAllowed('editusercss') )
+ || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
+ }
+ /**
+ * Protect js subpages of user pages: can $wgUser edit
* this page?
*
* @return \type{\bool} TRUE or FALSE
* @todo XXX: this might be better using restrictions
*/
- public function userCanEditCssJsSubpage() {
+ public function userCanEditJsSubpage() {
global $wgUser;
- return ( $wgUser->isAllowed('editusercssjs') || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
+ return ( ( $wgUser->isAllowed('editusercssjs') && $wgUser->isAllowed('edituserjs') )
+ || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) );
}
/**
@@ -1727,17 +1776,12 @@ class Title {
* Cascading protection: Get the source of any cascading restrictions on this page.
*
* @param $get_pages \type{\bool} Whether or not to retrieve the actual pages that the restrictions have come from.
- * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title objects of the pages from
+ * @return \type{\arrayof{mixed title array, restriction array}} Array of the Title objects of the pages from
* which cascading restrictions have come, false for none, or true if such restrictions exist, but $get_pages was not set.
* The restriction array is an array of each type, each of which contains an array of unique groups.
*/
public function getCascadeProtectionSources( $get_pages = true ) {
- global $wgRestrictionTypes;
-
- # Define our dimension of restrictions types
$pagerestrictions = array();
- foreach( $wgRestrictionTypes as $action )
- $pagerestrictions[$action] = array();
if ( isset( $this->mCascadeSources ) && $get_pages ) {
return array( $this->mCascadeSources, $this->mCascadingRestrictions );
@@ -1788,7 +1832,13 @@ class Title {
$sources[$page_id] = Title::makeTitle($page_ns, $page_title);
# Add groups needed for each restriction type if its not already there
# Make sure this restriction type still exists
- if ( isset($pagerestrictions[$row->pr_type]) && !in_array($row->pr_level, $pagerestrictions[$row->pr_type]) ) {
+
+ if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
+ $pagerestrictions[$row->pr_type] = array();
+ }
+
+ if ( isset($pagerestrictions[$row->pr_type]) &&
+ !in_array($row->pr_level, $pagerestrictions[$row->pr_type]) ) {
$pagerestrictions[$row->pr_type][]=$row->pr_level;
}
} else {
@@ -1826,11 +1876,23 @@ class Title {
* Loads a string into mRestrictions array
* @param $res \type{Resource} restrictions as an SQL result.
*/
- private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) {
- global $wgRestrictionTypes;
+ private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
+ $rows = array();
+ $dbr = wfGetDB( DB_SLAVE );
+
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $rows[] = $row;
+ }
+
+ $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
+ }
+
+ public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
$dbr = wfGetDB( DB_SLAVE );
- foreach( $wgRestrictionTypes as $type ){
+ $restrictionTypes = $this->getRestrictionTypes();
+
+ foreach( $restrictionTypes as $type ){
$this->mRestrictions[$type] = array();
$this->mRestrictionsExpiry[$type] = Block::decodeExpiry('');
}
@@ -1839,8 +1901,8 @@ class Title {
# Backwards-compatibility: also load the restrictions from the page record (old format).
- if ( $oldFashionedRestrictions === NULL ) {
- $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
+ if ( $oldFashionedRestrictions === null ) {
+ $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
array( 'page_id' => $this->getArticleId() ), __METHOD__ );
}
@@ -1861,16 +1923,17 @@ class Title {
}
- if( $dbr->numRows( $res ) ) {
+ if( count($rows) ) {
# Current system - load second to make them override.
$now = wfTimestampNow();
$purgeExpired = false;
- foreach( $res as $row ) {
+ foreach( $rows as $row ) {
# Cycle through all the restrictions.
- // Don't take care of restrictions types that aren't in $wgRestrictionTypes
- if( !in_array( $row->pr_type, $wgRestrictionTypes ) )
+ // Don't take care of restrictions types that aren't allowed
+
+ if( !in_array( $row->pr_type, $restrictionTypes ) )
continue;
// This code should be refactored, now that it's being used more generally,
@@ -1900,7 +1963,7 @@ class Title {
/**
* Load restrictions from the page_restrictions table
*/
- public function loadRestrictions( $oldFashionedRestrictions = NULL ) {
+ public function loadRestrictions( $oldFashionedRestrictions = null ) {
if( !$this->mRestrictionsLoaded ) {
if ($this->exists()) {
$dbr = wfGetDB( DB_SLAVE );
@@ -1908,7 +1971,7 @@ class Title {
$res = $dbr->select( 'page_restrictions', '*',
array ( 'pr_page' => $this->getArticleId() ), __METHOD__ );
- $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions );
+ $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
} else {
$title_protection = $this->getTitleProtection();
@@ -1964,7 +2027,7 @@ class Title {
/**
* Get the expiry time for the restriction against a given action
- * @return 14-char timestamp, or 'infinity' if the page is protected forever
+ * @return 14-char timestamp, or 'infinity' if the page is protected forever
* or not protected at all, or false if the action is not recognised.
*/
public function getRestrictionExpiry( $action ) {
@@ -1983,7 +2046,7 @@ class Title {
$n = 0;
} else {
$dbr = wfGetDB( DB_SLAVE );
- $n = $dbr->selectField( 'archive', 'COUNT(*)',
+ $n = $dbr->selectField( 'archive', 'COUNT(*)',
array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
__METHOD__
);
@@ -1996,7 +2059,7 @@ class Title {
}
return (int)$n;
}
-
+
/**
* Is there a version of this page in the deletion archive?
* @return bool
@@ -2023,7 +2086,7 @@ class Title {
* Get the article ID for this Title from the link cache,
* adding it if necessary
* @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select
- * for update
+ * for update
* @return \type{\int} the ID
*/
public function getArticleID( $flags = 0 ) {
@@ -2085,7 +2148,7 @@ class Title {
/**
* What is the page_latest field for this page?
* @param $flags \type{\int} a bit field; may be GAID_FOR_UPDATE to select for update
- * @return \type{\int}
+ * @return \type{\int} or false if the page doesn't exist
*/
public function getLatestRevID( $flags = 0 ) {
if( $this->mLatestID !== false )
@@ -2111,7 +2174,7 @@ class Title {
$linkCache->clearBadLink( $this->getPrefixedDBkey() );
if ( $newid === false ) { $this->mArticleID = -1; }
- else { $this->mArticleID = $newid; }
+ else { $this->mArticleID = intval( $newid ); }
$this->mRestrictionsLoaded = false;
$this->mRestrictions = array();
}
@@ -2126,8 +2189,8 @@ class Title {
}
$dbw = wfGetDB( DB_MASTER );
$success = $dbw->update( 'page',
- array( 'page_touched' => $dbw->timestamp() ),
- $this->pageCond(),
+ array( 'page_touched' => $dbw->timestamp() ),
+ $this->pageCond(),
__METHOD__
);
HTMLFileCache::clearFileCache( $this );
@@ -2144,7 +2207,7 @@ class Title {
*/
/* private */ function prefix( $name ) {
$p = '';
- if ( '' != $this->mInterwiki ) {
+ if ( $this->mInterwiki != '' ) {
$p = $this->mInterwiki . ':';
}
if ( 0 != $this->mNamespace ) {
@@ -2153,20 +2216,10 @@ class Title {
return $p . $name;
}
- /**
- * Secure and split - main initialisation function for this object
- *
- * Assumes that mDbkeyform has been set, and is urldecoded
- * and uses underscores, but not otherwise munged. This function
- * removes illegal characters, splits off the interwiki and
- * namespace prefixes, sets the other forms, and canonicalizes
- * everything.
- * @return \type{\bool} true on success
- */
- private function secureAndSplit() {
- global $wgContLang, $wgLocalInterwiki, $wgCapitalLinks;
-
- # Initialisation
+ // Returns a simple regex that will match on characters and sequences invalid in titles.
+ // Note that this doesn't pick up many things that could be wrong with titles, but that
+ // replacing this regex with something valid will make many titles valid.
+ static function getTitleInvalidRegex() {
static $rxTc = false;
if( !$rxTc ) {
# Matching titles will be held as illegal.
@@ -2183,6 +2236,37 @@ class Title {
'/S';
}
+ return $rxTc;
+ }
+
+ /**
+ * Capitalize a text if it belongs to a namespace that capitalizes
+ */
+ public static function capitalize( $text, $ns = NS_MAIN ) {
+ global $wgContLang;
+
+ if ( MWNamespace::isCapitalized( $ns ) )
+ return $wgContLang->ucfirst( $text );
+ else
+ return $text;
+ }
+
+ /**
+ * Secure and split - main initialisation function for this object
+ *
+ * Assumes that mDbkeyform has been set, and is urldecoded
+ * and uses underscores, but not otherwise munged. This function
+ * removes illegal characters, splits off the interwiki and
+ * namespace prefixes, sets the other forms, and canonicalizes
+ * everything.
+ * @return \type{\bool} true on success
+ */
+ private function secureAndSplit() {
+ global $wgContLang, $wgLocalInterwiki;
+
+ # Initialisation
+ $rxTc = self::getTitleInvalidRegex();
+
$this->mInterwiki = $this->mFragment = '';
$this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
@@ -2194,11 +2278,14 @@ class Title {
$dbkey = preg_replace( '/\xE2\x80[\x8E\x8F\xAA-\xAE]/S', '', $dbkey );
# Clean up whitespace
+ # Note: use of the /u option on preg_replace here will cause
+ # input with invalid UTF-8 sequences to be nullified out in PHP 5.2.x,
+ # conveniently disabling them.
#
- $dbkey = preg_replace( '/[ _]+/', '_', $dbkey );
+ $dbkey = preg_replace( '/[ _\xA0\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u', '_', $dbkey );
$dbkey = trim( $dbkey, '_' );
- if ( '' == $dbkey ) {
+ if ( $dbkey == '' ) {
return false;
}
@@ -2273,7 +2360,7 @@ class Title {
# We already know that some pages won't be in the database!
#
- if ( '' != $this->mInterwiki || NS_SPECIAL == $this->mNamespace ) {
+ if ( $this->mInterwiki != '' || NS_SPECIAL == $this->mNamespace ) {
$this->mArticleID = 0;
}
$fragment = strstr( $dbkey, '#' );
@@ -2337,8 +2424,8 @@ class Title {
* site might be case-sensitive.
*/
$this->mUserCaseDBKey = $dbkey;
- if( $wgCapitalLinks && $this->mInterwiki == '') {
- $dbkey = $wgContLang->ucfirst( $dbkey );
+ if( $this->mInterwiki == '') {
+ $dbkey = self::capitalize( $dbkey, $this->mNamespace );
}
/**
@@ -2375,7 +2462,7 @@ class Title {
/**
* Set the fragment for this title. Removes the first character from the
- * specified fragment before setting, so it assumes you're passing it with
+ * specified fragment before setting, so it assumes you're passing it with
* an initial "#".
*
* Deprecated for public use, use Title::makeTitle() with fragment parameter.
@@ -2487,8 +2574,8 @@ class Title {
),
__METHOD__, array(),
array(
- 'page' => array(
- 'LEFT JOIN',
+ 'page' => array(
+ 'LEFT JOIN',
array( 'pl_namespace=page_namespace', 'pl_title=page_title' )
)
)
@@ -2553,14 +2640,14 @@ class Title {
* Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
* @param &$nt \type{Title} the new title
* @param $auth \type{\bool} indicates whether $wgUser's permissions
- * should be checked
+ * should be checked
* @param $reason \type{\string} is the log summary of the move, used for spam checking
* @return \type{\mixed} True on success, getUserPermissionsErrors()-like array on failure
*/
public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
global $wgUser;
- $errors = array();
+ $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
@@ -2585,9 +2672,9 @@ class Title {
if ( strlen( $nt->getDBkey() ) < 1 ) {
$errors[] = array('articleexists');
}
- if ( ( '' == $this->getDBkey() ) ||
+ if ( ( $this->getDBkey() == '' ) ||
( !$oldid ) ||
- ( '' == $nt->getDBkey() ) ) {
+ ( $nt->getDBkey() == '' ) ) {
$errors[] = array('badarticleerror');
}
@@ -2601,10 +2688,15 @@ class Title {
if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
$errors[] = array('imageinvalidfilename');
}
- if( !File::checkExtensionCompatibility( $file, $nt->getDBKey() ) ) {
+ if( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
$errors[] = array('imagetypemismatch');
}
}
+ $destfile = wfLocalFile( $nt );
+ if( !$wgUser->isAllowed( 'reupload-shared' ) && !$destfile->exists() && wfFindFile( $nt ) ) {
+ $errors[] = array( 'file-exists-sharedrepo' );
+ }
+
}
if ( $auth ) {
@@ -2620,7 +2712,7 @@ class Title {
// This is kind of lame, won't display nice
$errors[] = array('spamprotectiontext');
}
-
+
$err = null;
if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
$errors[] = array('hookaborted', $err);
@@ -2650,7 +2742,7 @@ class Title {
* Move a title to a new location
* @param &$nt \type{Title} the new title
* @param $auth \type{\bool} indicates whether $wgUser's permissions
- * should be checked
+ * should be checked
* @param $reason \type{\string} The reason for the move
* @param $createRedirect \type{\bool} Whether to create a redirect from the old title to the new title.
* Ignored if the user doesn't have the suppressredirect right.
@@ -2662,6 +2754,18 @@ class Title {
return $err;
}
+ // If it is a file, move it first. It is done before all other moving stuff is done because it's hard to revert
+ $dbw = wfGetDB( DB_MASTER );
+ if( $this->getNamespace() == NS_FILE ) {
+ $file = wfLocalFile( $this );
+ if( $file->exists() ) {
+ $status = $file->move( $nt );
+ if( !$status->isOk() ) {
+ return $status->getErrorsArray();
+ }
+ }
+ }
+
$pageid = $this->getArticleID();
$protected = $this->isProtected();
if( $nt->exists() ) {
@@ -2688,7 +2792,6 @@ class Title {
// we can't actually distinguish it from a default here, and it'll
// be set to the new title even though it really shouldn't.
// It'll get corrected on the next edit, but resetting cl_timestamp.
- $dbw = wfGetDB( DB_MASTER );
$dbw->update( 'categorylinks',
array(
'cl_sortkey' => $nt->getPrefixedText(),
@@ -2701,7 +2804,7 @@ class Title {
if( $protected ) {
# Protect the redirect title as the title used to be...
$dbw->insertSelect( 'page_restrictions', 'page_restrictions',
- array(
+ array(
'pr_page' => $redirid,
'pr_type' => 'pr_type',
'pr_level' => 'pr_level',
@@ -2760,7 +2863,7 @@ class Title {
# @bug 17860: old article can be deleted, if this the case,
# delete it from message cache
- if ( $this->getArticleID === 0 ) {
+ if ( $this->getArticleID() === 0 ) {
$wgMessageCache->replace( $this->getDBkey(), false );
} else {
$oldarticle = new Article( $this );
@@ -2781,19 +2884,21 @@ class Title {
* source page
*
* @param &$nt \type{Title} the page to move to, which should currently
- * be a redirect
+ * be a redirect
* @param $reason \type{\string} The reason for the move
* @param $createRedirect \type{\bool} Whether to leave a redirect at the old title.
* Ignored if the user doesn't have the suppressredirect right
*/
private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) {
- global $wgUseSquid, $wgUser;
- $fname = 'Title::moveOverExistingRedirect';
+ global $wgUseSquid, $wgUser, $wgContLang;
+
$comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() );
if ( $reason ) {
- $comment .= ": $reason";
+ $comment .= wfMsgForContent( 'colon-separator' ) . $reason;
}
+ # Truncate for whole multibyte characters. +5 bytes for ellipsis
+ $comment = $wgContLang->truncate( $comment, 250 );
$now = wfTimestampNow();
$newid = $nt->getArticleID();
@@ -2802,11 +2907,15 @@ class Title {
$dbw = wfGetDB( DB_MASTER );
+ $rcts = $dbw->timestamp( $nt->getEarliestRevTime() );
+ $newns = $nt->getNamespace();
+ $newdbk = $nt->getDBkey();
+
# Delete the old redirect. We don't save it to history since
# by definition if we've got here it's rather uninteresting.
# We have to remove it so that the next step doesn't trigger
# a conflict on the unique namespace+title index...
- $dbw->delete( 'page', array( 'page_id' => $newid ), $fname );
+ $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
if ( !$dbw->cascadingDeletes() ) {
$dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ );
global $wgUseTrackbacks;
@@ -2820,11 +2929,16 @@ class Title {
$dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ );
$dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ );
}
+ // If the redirect was recently created, it may have an entry in recentchanges still
+ $dbw->delete( 'recentchanges',
+ array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ),
+ __METHOD__
+ );
# 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, $latest, $wgUser) );
@@ -2837,7 +2951,7 @@ class Title {
'page_latest' => $nullRevId,
),
/* WHERE */ array( 'page_id' => $oldid ),
- $fname
+ __METHOD__
);
$nt->resetArticleID( $oldid );
@@ -2853,36 +2967,24 @@ class Title {
'text' => $redirectText ) );
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
-
+
wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) );
# Now, we record the link from the redirect to the new title.
# It should have no other outgoing links...
- $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname );
+ $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ );
$dbw->insert( 'pagelinks',
array(
'pl_from' => $newid,
'pl_namespace' => $nt->getNamespace(),
'pl_title' => $nt->getDBkey() ),
- $fname );
+ __METHOD__ );
$redirectSuppressed = false;
} else {
$this->resetArticleID( 0 );
$redirectSuppressed = true;
}
- # Move an image if this is a file
- if( $this->getNamespace() == NS_FILE ) {
- $file = wfLocalFile( $this );
- if( $file->exists() ) {
- $status = $file->move( $nt );
- if( !$status->isOk() ) {
- $dbw->rollback();
- return $status->getErrorsArray();
- }
- }
- }
-
# Log the move
$log = new LogPage( 'move' );
$log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
@@ -2893,7 +2995,7 @@ class Title {
$u = new SquidUpdate( $urls );
$u->doUpdate();
}
-
+
}
/**
@@ -2904,26 +3006,31 @@ class Title {
* Ignored if the user doesn't have the suppressredirect right
*/
private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) {
- global $wgUseSquid, $wgUser;
- $fname = 'MovePageForm::moveToNewTitle';
+ global $wgUseSquid, $wgUser, $wgContLang;
+
$comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() );
if ( $reason ) {
$comment .= wfMsgExt( 'colon-separator',
array( 'escapenoentities', 'content' ) );
$comment .= $reason;
}
+ # Truncate for whole multibyte characters. +5 bytes for ellipsis
+ $comment = $wgContLang->truncate( $comment, 250 );
$newid = $nt->getArticleID();
$oldid = $this->getArticleID();
$latest = $this->getLatestRevId();
-
+
$dbw = wfGetDB( DB_MASTER );
$now = $dbw->timestamp();
# Save a null revision in the page's history notifying of the move
$nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true );
+ if ( !is_object( $nullRevision ) ) {
+ throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
+ }
$nullRevId = $nullRevision->insertOn( $dbw );
-
+
$article = new Article( $this );
wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
@@ -2936,7 +3043,7 @@ class Title {
'page_latest' => $nullRevId,
),
/* WHERE */ array( 'page_id' => $oldid ),
- $fname
+ __METHOD__
);
$nt->resetArticleID( $oldid );
@@ -2952,7 +3059,7 @@ class Title {
'text' => $redirectText ) );
$redirectRevision->insertOn( $dbw );
$redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
-
+
wfRunHooks( 'NewRevisionFromEditComplete', array($redirectArticle, $redirectRevision, false, $wgUser) );
# Record the just-created redirect's linking to the page
@@ -2961,25 +3068,13 @@ class Title {
'pl_from' => $newid,
'pl_namespace' => $nt->getNamespace(),
'pl_title' => $nt->getDBkey() ),
- $fname );
+ __METHOD__ );
$redirectSuppressed = false;
} else {
$this->resetArticleID( 0 );
$redirectSuppressed = true;
}
- # Move an image if this is a file
- if( $this->getNamespace() == NS_FILE ) {
- $file = wfLocalFile( $this );
- if( $file->exists() ) {
- $status = $file->move( $nt );
- if( !$status->isOk() ) {
- $dbw->rollback();
- return $status->getErrorsArray();
- }
- }
- }
-
# Log the move
$log = new LogPage( 'move' );
$log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) );
@@ -2990,9 +3085,9 @@ class Title {
# Purge old title from squid
# The new title, and links to the new title, are purged in Article::onArticleCreate()
$this->purgeSquid();
-
+
}
-
+
/**
* Move this page's subpages to be subpages of $nt
* @param $nt Title Move target
@@ -3004,7 +3099,7 @@ class Title {
* arrays (errors) as values, or an error array with numeric indices if no pages were moved
*/
public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
- global $wgUser, $wgMaximumMovedPages;
+ global $wgMaximumMovedPages;
// Check permissions
if( !$this->userCan( 'move-subpages' ) )
return array( 'cant-move-subpages' );
@@ -3028,13 +3123,18 @@ class Title {
break;
}
- if( $oldSubpage->getArticleId() == $this->getArticleId() )
+ // We don't know whether this function was called before
+ // or after moving the root page, so check both
+ // $this and $nt
+ if( $oldSubpage->getArticleId() == $this->getArticleId() ||
+ $oldSubpage->getArticleID() == $nt->getArticleId() )
// When moving a page to a subpage of itself,
// don't move it twice
continue;
$newPageName = preg_replace(
- '#^'.preg_quote( $this->getDBKey(), '#' ).'#',
- $nt->getDBKey(), $oldSubpage->getDBKey() );
+ '#^'.preg_quote( $this->getDBkey(), '#' ).'#',
+ StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
+ $oldSubpage->getDBkey() );
if( $oldSubpage->isTalkPage() ) {
$newNs = $nt->getTalkPage()->getNamespace();
} else {
@@ -3053,7 +3153,7 @@ class Title {
}
return $retval;
}
-
+
/**
* Checks if this page is just a one-rev redirect.
* Adds lock, so don't use just for light purposes.
@@ -3083,7 +3183,7 @@ class Title {
'page_title' => $this->getDBkey(),
'page_id=rev_page',
'page_latest != rev_id'
- ),
+ ),
__METHOD__,
array( 'FOR UPDATE' )
);
@@ -3183,7 +3283,7 @@ class Title {
* @return \type{\array} Tree of parent categories
*/
public function getParentCategoryTree( $children = array() ) {
- $stack = array();
+ $stack = array();
$parents = $this->getParentCategories();
if( $parents ) {
@@ -3257,7 +3357,7 @@ class Title {
array( 'ORDER BY' => 'rev_id' )
);
}
-
+
/**
* Get the first revision of the page
*
@@ -3267,19 +3367,19 @@ class Title {
public function getFirstRevision( $flags=0 ) {
$db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
$pageId = $this->getArticleId($flags);
- if( !$pageId ) return NULL;
+ if( !$pageId ) return null;
$row = $db->selectRow( 'revision', '*',
array( 'rev_page' => $pageId ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
);
if( !$row ) {
- return NULL;
+ return null;
} else {
return new Revision( $row );
}
}
-
+
/**
* Check if this is a new page
*
@@ -3317,7 +3417,7 @@ class Title {
*/
public function countRevisionsBetween( $old, $new ) {
$dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'revision', 'count(*)',
+ return (int)$dbr->selectField( 'revision', 'count(*)',
'rev_page = ' . intval( $this->getArticleId() ) .
' AND rev_id > ' . intval( $old ) .
' AND rev_id < ' . intval( $new ),
@@ -3396,7 +3496,7 @@ class Title {
case NS_FILE:
return wfFindFile( $this ); // file exists, possibly in a foreign repo
case NS_SPECIAL:
- return SpecialPage::exists( $this->getDBKey() ); // valid special page
+ return SpecialPage::exists( $this->getDBkey() ); // valid special page
case NS_MAIN:
return $this->mDbkeyform == ''; // selflink, possibly with fragment
case NS_MEDIAWIKI:
@@ -3423,7 +3523,7 @@ class Title {
public function isKnown() {
return $this->exists() || $this->isAlwaysKnown();
}
-
+
/**
* Is this in a namespace that allows actual pages?
*
@@ -3453,7 +3553,7 @@ class Title {
* @param Database $db, optional db
* @return \type{\string} Last touched timestamp
*/
- public function getTouched( $db = NULL ) {
+ public function getTouched( $db = null ) {
$db = isset($db) ? $db : wfGetDB( DB_SLAVE );
$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
return $touched;
@@ -3464,7 +3564,7 @@ class Title {
* @param User $user
* @return mixed string/NULL
*/
- public function getNotificationTimestamp( $user = NULL ) {
+ public function getNotificationTimestamp( $user = null ) {
global $wgUser, $wgShowUpdatedMarker;
// Assume current user if none given
if( !$user ) $user = $wgUser;
@@ -3534,40 +3634,36 @@ class Title {
* Generate strings used for xml 'id' names in monobook tabs
* @return \type{\string} XML 'id' name
*/
- public function getNamespaceKey() {
+ public function getNamespaceKey( $prepend = 'nstab-' ) {
global $wgContLang;
- switch ($this->getNamespace()) {
- case NS_MAIN:
- case NS_TALK:
- return 'nstab-main';
- case NS_USER:
- case NS_USER_TALK:
- return 'nstab-user';
- case NS_MEDIA:
- return 'nstab-media';
- case NS_SPECIAL:
- return 'nstab-special';
- case NS_PROJECT:
- case NS_PROJECT_TALK:
- return 'nstab-project';
- case NS_FILE:
- case NS_FILE_TALK:
- return 'nstab-image';
- case NS_MEDIAWIKI:
- case NS_MEDIAWIKI_TALK:
- return 'nstab-mediawiki';
- case NS_TEMPLATE:
- case NS_TEMPLATE_TALK:
- return 'nstab-template';
- case NS_HELP:
- case NS_HELP_TALK:
- return 'nstab-help';
- case NS_CATEGORY:
- case NS_CATEGORY_TALK:
- return 'nstab-category';
- default:
- return 'nstab-' . $wgContLang->lc( $this->getSubjectNsText() );
+ // Gets the subject namespace if this title
+ $namespace = MWNamespace::getSubject( $this->getNamespace() );
+ // Checks if cononical namespace name exists for namespace
+ if ( MWNamespace::exists( $this->getNamespace() ) ) {
+ // Uses canonical namespace name
+ $namespaceKey = MWNamespace::getCanonicalName( $namespace );
+ } else {
+ // Uses text of namespace
+ $namespaceKey = $this->getSubjectNsText();
+ }
+ // Makes namespace key lowercase
+ $namespaceKey = $wgContLang->lc( $namespaceKey );
+ // Uses main
+ if ( $namespaceKey == '' ) {
+ $namespaceKey = 'main';
+ }
+ // Changes file to image for backwards compatibility
+ if ( $namespaceKey == 'file' ) {
+ $namespaceKey = 'image';
}
+ return $prepend . $namespaceKey;
+ }
+
+ /**
+ * Returns true if this is a special page.
+ */
+ public function isSpecialPage( ) {
+ return $this->getNamespace() == NS_SPECIAL;
}
/**
@@ -3615,21 +3711,21 @@ class Title {
/**
* Get all extant redirects to this Title
*
- * @param $ns \twotypes{\int,\null} Single namespace to consider;
+ * @param $ns \twotypes{\int,\null} Single namespace to consider;
* NULL to consider all namespaces
* @return \type{\arrayof{Title}} Redirects to this title
*/
public function getRedirectsHere( $ns = null ) {
$redirs = array();
-
- $dbr = wfGetDB( DB_SLAVE );
+
+ $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;
-
+
$res = $dbr->select(
array( 'redirect', 'page' ),
array( 'page_namespace', 'page_title' ),
@@ -3643,7 +3739,7 @@ class Title {
}
return $redirs;
}
-
+
/**
* Check if this Title is a valid redirect target
*
@@ -3651,18 +3747,18 @@ class Title {
*/
public function isValidRedirectTarget() {
global $wgInvalidRedirectTargets;
-
+
// invalid redirect targets are stored in a global array, but explicity disallow Userlogout here
if( $this->isSpecial( 'Userlogout' ) ) {
return false;
}
-
+
foreach( $wgInvalidRedirectTargets as $target ) {
if( $this->isSpecial( $target ) ) {
return false;
}
}
-
+
return true;
}
@@ -3675,4 +3771,34 @@ class Title {
}
return $this->mBacklinkCache;
}
+
+ /**
+ * Whether the magic words __INDEX__ and __NOINDEX__ function for
+ * this page.
+ * @return Bool
+ */
+ public function canUseNoindex(){
+ global $wgArticleRobotPolicies, $wgContentNamespaces,
+ $wgExemptFromUserRobotsControl;
+
+ $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
+ ? $wgContentNamespaces
+ : $wgExemptFromUserRobotsControl;
+
+ return !in_array( $this->mNamespace, $bannedNamespaces );
+
+ }
+
+ public function getRestrictionTypes() {
+ global $wgRestrictionTypes;
+ $types = $this->exists() ? $wgRestrictionTypes : array('create');
+
+ if ( $this->getNamespace() == NS_FILE ) {
+ $types[] = 'upload';
+ }
+
+ wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
+
+ return $types;
+ }
}
diff --git a/includes/User.php b/includes/User.php
index 2ccc695b..51ffe70a 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -14,7 +14,7 @@ define( 'USER_TOKEN_LENGTH', 32 );
* \int Serialized record version.
* @ingroup Constants
*/
-define( 'MW_USER_VERSION', 6 );
+define( 'MW_USER_VERSION', 8 );
/**
* \string Some punctuation to prevent editing from broken text-mangling proxies.
@@ -43,8 +43,8 @@ class PasswordError extends MWException {
class User {
/**
- * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user
- * preferences that are displayed by Special:Preferences as checkboxes.
+ * \type{\arrayof{\string}} A list of default user toggles, i.e., boolean user
+ * preferences that are displayed by Special:Preferences as checkboxes.
* This list can be extended via the UserToggles hook or by
* $wgContLang::getExtraUserToggles().
* @showinitializer
@@ -95,8 +95,8 @@ class User {
);
/**
- * \type{\arrayof{\string}} List of member variables which are saved to the
- * shared cache (memcached). Any operation which changes the
+ * \type{\arrayof{\string}} List of member variables which are saved to the
+ * shared cache (memcached). Any operation which changes the
* corresponding database fields must call a cache-clearing function.
* @showinitializer
*/
@@ -109,7 +109,6 @@ class User {
'mNewpassword',
'mNewpassTime',
'mEmail',
- 'mOptions',
'mTouched',
'mToken',
'mEmailAuthenticated',
@@ -119,11 +118,13 @@ class User {
'mEditCount',
// user_group table
'mGroups',
+ // user_properties table
+ 'mOptionOverrides',
);
/**
* \type{\arrayof{\string}} Core rights.
- * Each of these should have a corresponding message of the form
+ * Each of these should have a corresponding message of the form
* "right-$right".
* @showinitializer
*/
@@ -141,6 +142,7 @@ class User {
'createtalk',
'delete',
'deletedhistory',
+ 'deletedtext',
'deleterevision',
'edit',
'editinterface',
@@ -166,6 +168,7 @@ class User {
'reupload',
'reupload-shared',
'rollback',
+ 'sendemail',
'siteadmin',
'suppressionlog',
'suppressredirect',
@@ -187,14 +190,14 @@ class User {
/** @name Cache variables */
//@{
var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
- $mEmail, $mOptions, $mTouched, $mToken, $mEmailAuthenticated,
- $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups;
+ $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
+ $mEmailToken, $mEmailTokenExpires, $mRegistration, $mGroups, $mOptionOverrides;
//@}
/**
* \bool Whether the cache variables have been loaded.
*/
- var $mDataLoaded, $mAuthLoaded;
+ var $mDataLoaded, $mAuthLoaded, $mOptionsLoaded;
/**
* \string Initialization data source if mDataLoaded==false. May be one of:
@@ -210,14 +213,16 @@ class User {
/** @name Lazy-initialized variables, invalidated with clearInstanceCache */
//@{
var $mNewtalk, $mDatePreference, $mBlockedby, $mHash, $mSkin, $mRights,
- $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
- $mLocked, $mHideName;
+ $mBlockreason, $mBlock, $mEffectiveGroups, $mBlockedGlobally,
+ $mLocked, $mHideName, $mOptions;
//@}
+ static $idCacheByName = array();
+
/**
* Lightweight constructor for an anonymous user.
* Use the User::newFrom* factory functions for other kinds of users.
- *
+ *
* @see newFromName()
* @see newFromId()
* @see newFromConfirmationCode()
@@ -310,6 +315,7 @@ class User {
function saveToCache() {
$this->load();
$this->loadGroups();
+ $this->loadOptions();
if ( $this->isAnon() ) {
// Anonymous users are uncached
return;
@@ -323,8 +329,8 @@ class User {
global $wgMemc;
$wgMemc->set( $key, $data );
}
-
-
+
+
/** @name newFrom*() static factory methods */
//@{
@@ -339,8 +345,9 @@ class User {
* User::getCanonicalName(), except that true is accepted as an alias
* for 'valid', for BC.
*
- * @return \type{User} The User object, or null if the username is invalid. If the
- * username is not present in the database, the result will be a user object
+ * @return \type{User} The User object, or false if the username is invalid
+ * (e.g. if it contains illegal characters or is an IP address). If the
+ * username is not present in the database, the result will be a user object
* with a name, zero user ID and default settings.
*/
static function newFromName( $name, $validate = 'valid' ) {
@@ -349,7 +356,7 @@ class User {
}
$name = self::getCanonicalName( $name, $validate );
if ( $name === false ) {
- return null;
+ return false;
} else {
# Create unloaded user object
$u = new User;
@@ -418,9 +425,9 @@ class User {
$user->loadFromRow( $row );
return $user;
}
-
+
//@}
-
+
/**
* Get the username corresponding to a given user ID
@@ -429,7 +436,7 @@ class User {
*/
static function whoIs( $id ) {
$dbr = wfGetDB( DB_SLAVE );
- return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), 'User::whoIs' );
+ return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ), __METHOD__ );
}
/**
@@ -454,14 +461,27 @@ class User {
# Illegal name
return null;
}
+
+ if ( isset( self::$idCacheByName[$name] ) ) {
+ return self::$idCacheByName[$name];
+ }
+
$dbr = wfGetDB( DB_SLAVE );
$s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), __METHOD__ );
if ( $s === false ) {
- return 0;
+ $result = null;
} else {
- return $s->user_id;
+ $result = $s->user_id;
+ }
+
+ self::$idCacheByName[$name] = $result;
+
+ if ( count( self::$idCacheByName ) > 1000 ) {
+ self::$idCacheByName = array();
}
+
+ return $result;
}
/**
@@ -599,21 +619,45 @@ class User {
/**
* Is the input a valid password for this user?
*
- * @param $password \string Desired password
- * @return \bool True or false
+ * @param $password String Desired password
+ * @return bool True or false
*/
function isValidPassword( $password ) {
+ //simple boolean wrapper for getPasswordValidity
+ return $this->getPasswordValidity( $password ) === true;
+ }
+
+ /**
+ * Given unvalidated password input, return error message on failure.
+ *
+ * @param $password String Desired password
+ * @return mixed: true on success, string of error message on failure
+ */
+ function getPasswordValidity( $password ) {
global $wgMinimalPasswordLength, $wgContLang;
- $result = null;
+ $result = false; //init $result to false for the internal checks
+
if( !wfRunHooks( 'isValidPassword', array( $password, &$result, $this ) ) )
return $result;
- if( $result === false )
- return false;
- // Password needs to be long enough, and can't be the same as the username
- return strlen( $password ) >= $wgMinimalPasswordLength
- && $wgContLang->lc( $password ) !== $wgContLang->lc( $this->mName );
+ if ( $result === false ) {
+ if( strlen( $password ) < $wgMinimalPasswordLength ) {
+ return 'passwordtooshort';
+ } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) {
+ return 'password-name-match';
+ } else {
+ //it seems weird returning true here, but this is because of the
+ //initialization of $result to false above. If the hook is never run or it
+ //doesn't modify $result, then we will likely get down into this if with
+ //a valid password.
+ return true;
+ }
+ } elseif( $result === true ) {
+ return true;
+ } else {
+ return $result; //the isValidPassword hook set a string $result and returned true
+ }
}
/**
@@ -659,7 +703,7 @@ class User {
return false;
# Clean up name according to title rules
- $t = ($validate === 'valid') ?
+ $t = ( $validate === 'valid' ) ?
Title::newFromText( $name ) : Title::makeTitle( NS_USER, $name );
# Check for invalid titles
if( is_null( $t ) ) {
@@ -690,7 +734,7 @@ class User {
}
break;
default:
- throw new MWException( 'Invalid parameter value for $validate in '.__METHOD__ );
+ throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ );
}
return $name;
}
@@ -744,18 +788,18 @@ class User {
$l = strlen( $pwchars ) - 1;
$pwlength = max( 7, $wgMinimalPasswordLength );
- $digit = mt_rand(0, $pwlength - 1);
+ $digit = mt_rand( 0, $pwlength - 1 );
$np = '';
for ( $i = 0; $i < $pwlength; $i++ ) {
- $np .= $i == $digit ? chr( mt_rand(48, 57) ) : $pwchars{ mt_rand(0, $l)};
+ $np .= $i == $digit ? chr( mt_rand( 48, 57 ) ) : $pwchars{ mt_rand( 0, $l ) };
}
return $np;
}
/**
- * Set cached properties to default.
+ * Set cached properties to default.
*
- * @note This no longer clears uncached lazy-initialised properties;
+ * @note This no longer clears uncached lazy-initialised properties;
* the constructor does that instead.
* @private
*/
@@ -770,7 +814,8 @@ class User {
$this->mPassword = $this->mNewpassword = '';
$this->mNewpassTime = null;
$this->mEmail = '';
- $this->mOptions = null; # Defer init
+ $this->mOptionOverrides = null;
+ $this->mOptionsLoaded = false;
if ( isset( $_COOKIE[$wgCookiePrefix.'LoggedOut'] ) ) {
$this->mTouched = wfTimestamp( TS_MW, $_COOKIE[$wgCookiePrefix.'LoggedOut'] );
@@ -804,7 +849,7 @@ class User {
* @return \bool True if the user is logged in, false otherwise.
*/
private function loadFromSession() {
- global $wgMemc, $wgCookiePrefix;
+ global $wgMemc, $wgCookiePrefix, $wgExternalAuthType, $wgAutocreatePolicy;
$result = null;
wfRunHooks( 'UserLoadFromSession', array( $this, &$result ) );
@@ -812,11 +857,19 @@ class User {
return $result;
}
+ if ( $wgExternalAuthType && $wgAutocreatePolicy == 'view' ) {
+ $extUser = ExternalUser::newFromCookie();
+ if ( $extUser ) {
+ # TODO: Automatically create the user here (or probably a bit
+ # lower down, in fact)
+ }
+ }
+
if ( isset( $_COOKIE["{$wgCookiePrefix}UserID"] ) ) {
$sId = intval( $_COOKIE["{$wgCookiePrefix}UserID"] );
if( isset( $_SESSION['wsUserID'] ) && $sId != $_SESSION['wsUserID'] ) {
$this->loadDefaults(); // Possible collision!
- wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
+ wfDebugLog( 'loginSessions', "Session user ID ({$_SESSION['wsUserID']}) and
cookie user ID ($sId) don't match!" );
return false;
}
@@ -850,6 +903,13 @@ class User {
return false;
}
+ global $wgBlockDisablesLogin;
+ if( $wgBlockDisablesLogin && $this->isBlocked() ) {
+ # User blocked and we've disabled blocked user logins
+ $this->loadDefaults();
+ return false;
+ }
+
if ( isset( $_SESSION['wsToken'] ) ) {
$passwordCorrect = $_SESSION['wsToken'] == $this->mToken;
$from = 'session';
@@ -934,7 +994,7 @@ class User {
$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;
+ $this->mEditCount = $row->user_editcount;
}
/**
@@ -969,6 +1029,7 @@ class User {
$this->mSkin = null;
$this->mRights = null;
$this->mEffectiveGroups = null;
+ $this->mOptions = null;
if ( $reloadFrom ) {
$this->mDataLoaded = false;
@@ -987,7 +1048,7 @@ class User {
/**
* Site defaults will override the global/language defaults
*/
- global $wgDefaultUserOptions, $wgContLang;
+ global $wgDefaultUserOptions, $wgContLang, $wgDefaultSkin;
$defOpt = $wgDefaultUserOptions + $wgContLang->getDefaultUserOptionOverrides();
/**
@@ -996,10 +1057,11 @@ class User {
$variant = $wgContLang->getPreferredVariant( false );
$defOpt['variant'] = $variant;
$defOpt['language'] = $variant;
-
- foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
- $defOpt['searchNs'.$nsnum] = $val;
+ foreach( SearchEngine::searchableNamespaces() as $nsnum => $nsname ) {
+ $defOpt['searchNs'.$nsnum] = !empty( $wgNamespacesToBeSearchedDefault[$nsnum] );
}
+ $defOpt['skin'] = $wgDefaultSkin;
+
return $defOpt;
}
@@ -1014,7 +1076,7 @@ class User {
if( isset( $defOpts[$opt] ) ) {
return $defOpts[$opt];
} else {
- return '';
+ return null;
}
}
@@ -1044,7 +1106,7 @@ class User {
* done against master.
*/
function getBlockedStatus( $bFromSlave = true ) {
- global $wgEnableSorbs, $wgProxyWhitelist;
+ global $wgProxyWhitelist, $wgUser;
if ( -1 != $this->mBlockedby ) {
wfDebug( "User::getBlockedStatus: already loaded.\n" );
@@ -1060,13 +1122,27 @@ class User {
// due to -1 !== 0. Probably session-related... Nothing should be
// overwriting mBlockedby, surely?
$this->load();
-
+
$this->mBlockedby = 0;
$this->mHideName = 0;
$this->mAllowUsertalk = 0;
- $ip = wfGetIP();
- if ($this->isAllowed( 'ipblock-exempt' ) ) {
+ # Check if we are looking at an IP or a logged-in user
+ if ( $this->isIP( $this->getName() ) ) {
+ $ip = $this->getName();
+ } else {
+ # Check if we are looking at the current user
+ # If we don't, and the user is logged in, we don't know about
+ # his IP / autoblock status, so ignore autoblock of current user's IP
+ if ( $this->getID() != $wgUser->getID() ) {
+ $ip = '';
+ } else {
+ # Get IP of current user
+ $ip = wfGetIP();
+ }
+ }
+
+ if ( $this->isAllowed( 'ipblock-exempt' ) ) {
# Exempt from all types of IP-block
$ip = '';
}
@@ -1075,22 +1151,24 @@ class User {
$this->mBlock = new Block();
$this->mBlock->fromMaster( !$bFromSlave );
if ( $this->mBlock->load( $ip , $this->mId ) ) {
- wfDebug( __METHOD__.": Found block.\n" );
+ wfDebug( __METHOD__ . ": Found block.\n" );
$this->mBlockedby = $this->mBlock->mBy;
+ if( $this->mBlockedby == "0" )
+ $this->mBlockedby = $this->mBlock->mByName;
$this->mBlockreason = $this->mBlock->mReason;
$this->mHideName = $this->mBlock->mHideName;
$this->mAllowUsertalk = $this->mBlock->mAllowUsertalk;
- if ( $this->isLoggedIn() ) {
+ if ( $this->isLoggedIn() && $wgUser->getID() == $this->getID() ) {
$this->spreadBlock();
}
} else {
- // Bug 13611: don't remove mBlock here, to allow account creation blocks to
- // apply to users. Note that the existence of $this->mBlock is not used to
+ // Bug 13611: don't remove mBlock here, to allow account creation blocks to
+ // apply to users. Note that the existence of $this->mBlock is not used to
// check for edit blocks, $this->mBlockedby is instead.
}
# Proxy blocking
- if ( !$this->isAllowed('proxyunbannable') && !in_array( $ip, $wgProxyWhitelist ) ) {
+ if ( !$this->isAllowed( 'proxyunbannable' ) && !in_array( $ip, $wgProxyWhitelist ) ) {
# Local list
if ( wfIsLocallyBlockedProxy( $ip ) ) {
$this->mBlockedby = wfMsg( 'proxyblocker' );
@@ -1098,8 +1176,8 @@ class User {
}
# DNSBL
- if ( !$this->mBlockedby && $wgEnableSorbs && !$this->getID() ) {
- if ( $this->inSorbsBlacklist( $ip ) ) {
+ if ( !$this->mBlockedby && !$this->getID() ) {
+ if ( $this->isDnsBlacklisted( $ip ) ) {
$this->mBlockedby = wfMsg( 'sorbs' );
$this->mBlockreason = wfMsg( 'sorbsreason' );
}
@@ -1113,43 +1191,57 @@ class User {
}
/**
- * Whether the given IP is in the SORBS blacklist.
+ * Whether the given IP is in a DNS blacklist.
*
* @param $ip \string IP to check
+ * @param $checkWhitelist Boolean: whether to check the whitelist first
* @return \bool True if blacklisted.
*/
- function inSorbsBlacklist( $ip ) {
- global $wgEnableSorbs, $wgSorbsUrl;
+ function isDnsBlacklisted( $ip, $checkWhitelist = false ) {
+ global $wgEnableSorbs, $wgEnableDnsBlacklist,
+ $wgSorbsUrl, $wgDnsBlacklistUrls, $wgProxyWhitelist;
+
+ if ( !$wgEnableDnsBlacklist && !$wgEnableSorbs )
+ return false;
- return $wgEnableSorbs &&
- $this->inDnsBlacklist( $ip, $wgSorbsUrl );
+ if ( $checkWhitelist && in_array( $ip, $wgProxyWhitelist ) )
+ return false;
+
+ $urls = array_merge( $wgDnsBlacklistUrls, (array)$wgSorbsUrl );
+ return $this->inDnsBlacklist( $ip, $urls );
}
/**
* Whether the given IP is in a given DNS blacklist.
*
* @param $ip \string IP to check
- * @param $base \string URL of the DNS blacklist
+ * @param $bases \string or Array of Strings: URL of the DNS blacklist
* @return \bool True if blacklisted.
*/
- function inDnsBlacklist( $ip, $base ) {
+ function inDnsBlacklist( $ip, $bases ) {
wfProfileIn( __METHOD__ );
$found = false;
$host = '';
// FIXME: IPv6 ??? (http://bugs.php.net/bug.php?id=33170)
- if( IP::isIPv4($ip) ) {
- # Make hostname
- $host = "$ip.$base";
+ if( IP::isIPv4( $ip ) ) {
+ # Reverse IP, bug 21255
+ $ipReversed = implode( '.', array_reverse( explode( '.', $ip ) ) );
- # Send query
- $ipList = gethostbynamel( $host );
+ foreach( (array)$bases as $base ) {
+ # Make hostname
+ $host = "$ipReversed.$base";
- if( $ipList ) {
- wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
- $found = true;
- } else {
- wfDebug( "Requested $host, not found in $base.\n" );
+ # Send query
+ $ipList = gethostbynamel( $host );
+
+ if( $ipList ) {
+ wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
+ $found = true;
+ break;
+ } else {
+ wfDebug( "Requested $host, not found in $base.\n" );
+ }
}
}
@@ -1188,8 +1280,7 @@ class User {
* @param $action \string Action to enforce; 'edit' if unspecified
* @return \bool True if a rate limiter was tripped
*/
- function pingLimiter( $action='edit' ) {
-
+ function pingLimiter( $action = 'edit' ) {
# Call the 'PingLimiter' hook
$result = false;
if( !wfRunHooks( 'PingLimiter', array( &$this, $action, $result ) ) ) {
@@ -1245,7 +1336,7 @@ class User {
}
// Set the user limit key
if ( $userLimit !== false ) {
- wfDebug( __METHOD__.": effective user limit: $userLimit\n" );
+ wfDebug( __METHOD__ . ": effective user limit: $userLimit\n" );
$keys[ wfMemcKey( 'limiter', $action, 'user', $id ) ] = $userLimit;
}
@@ -1254,19 +1345,20 @@ class User {
list( $max, $period ) = $limit;
$summary = "(limit $max in {$period}s)";
$count = $wgMemc->get( $key );
+ // Already pinged?
if( $count ) {
if( $count > $max ) {
- wfDebug( __METHOD__.": tripped! $key at $count $summary\n" );
+ wfDebug( __METHOD__ . ": tripped! $key at $count $summary\n" );
if( $wgRateLimitLog ) {
@error_log( wfTimestamp( TS_MW ) . ' ' . wfWikiID() . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
}
$triggered = true;
} else {
- wfDebug( __METHOD__.": ok. $key at $count $summary\n" );
+ wfDebug( __METHOD__ . ": ok. $key at $count $summary\n" );
}
} else {
- wfDebug( __METHOD__.": adding record for $key $summary\n" );
- $wgMemc->add( $key, 1, intval( $period ) );
+ wfDebug( __METHOD__ . ": adding record for $key $summary\n" );
+ $wgMemc->add( $key, 0, intval( $period ) ); // first ping
}
$wgMemc->incr( $key );
}
@@ -1277,7 +1369,7 @@ class User {
/**
* Check if user is blocked
- *
+ *
* @param $bFromSlave \bool Whether to check the slave database instead of the master
* @return \bool True if blocked, false otherwise
*/
@@ -1289,7 +1381,7 @@ class User {
/**
* Check if user is blocked from editing a particular article
- *
+ *
* @param $title \string Title to check
* @param $bFromSlave \bool Whether to check the slave database instead of the master
* @return \bool True if blocked, false otherwise
@@ -1297,17 +1389,20 @@ class User {
function isBlockedFrom( $title, $bFromSlave = false ) {
global $wgBlockAllowsUTEdit;
wfProfileIn( __METHOD__ );
- wfDebug( __METHOD__.": enter\n" );
+ wfDebug( __METHOD__ . ": enter\n" );
- wfDebug( __METHOD__.": asking isBlocked()\n" );
+ wfDebug( __METHOD__ . ": asking isBlocked()\n" );
$blocked = $this->isBlocked( $bFromSlave );
- $allowUsertalk = ($wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false);
+ $allowUsertalk = ( $wgBlockAllowsUTEdit ? $this->mAllowUsertalk : false );
# If a user's name is suppressed, they cannot make edits anywhere
if ( !$this->mHideName && $allowUsertalk && $title->getText() === $this->getName() &&
$title->getNamespace() == NS_USER_TALK ) {
$blocked = false;
- wfDebug( __METHOD__.": self-talk page, ignoring any blocks\n" );
+ wfDebug( __METHOD__ . ": self-talk page, ignoring any blocks\n" );
}
+
+ wfRunHooks( 'UserIsBlockedFrom', array( $this, $title, &$blocked, &$allowUsertalk ) );
+
wfProfileOut( __METHOD__ );
return $blocked;
}
@@ -1329,21 +1424,21 @@ class User {
$this->getBlockedStatus();
return $this->mBlockreason;
}
-
+
/**
* If user is blocked, return the ID for the block
* @return \int Block ID
*/
function getBlockId() {
$this->getBlockedStatus();
- return ($this->mBlock ? $this->mBlock->mId : false);
+ return ( $this->mBlock ? $this->mBlock->mId : false );
}
-
+
/**
* Check if user is blocked on all wikis.
* Do not use for actual edit permission checks!
* This is intented for quick UI checks.
- *
+ *
* @param $ip \type{\string} IP address, uses current client if none given
* @return \type{\bool} True if blocked, false otherwise
*/
@@ -1362,10 +1457,10 @@ class User {
$this->mBlockedGlobally = (bool)$blocked;
return $this->mBlockedGlobally;
}
-
+
/**
* Check if user account is locked
- *
+ *
* @return \type{\bool} True if locked, false otherwise
*/
function isLocked() {
@@ -1377,10 +1472,10 @@ class User {
$this->mLocked = (bool)$authUser->isLocked();
return $this->mLocked;
}
-
+
/**
* Check if user account is hidden
- *
+ *
* @return \type{\bool} True if hidden, false otherwise
*/
function isHidden() {
@@ -1504,17 +1599,16 @@ class User {
*/
function getNewMessageLinks() {
$talks = array();
- if (!wfRunHooks('UserRetrieveNewTalks', array(&$this, &$talks)))
+ if( !wfRunHooks( 'UserRetrieveNewTalks', array( &$this, &$talks ) ) )
return $talks;
- if (!$this->getNewtalk())
+ if( !$this->getNewtalk() )
return array();
$up = $this->getUserPage();
$utp = $up->getTalkPage();
- return array(array("wiki" => wfWikiID(), "link" => $utp->getLocalURL()));
+ return array( array( 'wiki' => wfWikiID(), 'link' => $utp->getLocalURL() ) );
}
-
/**
* Internal uncached check for new messages
*
@@ -1550,10 +1644,10 @@ class User {
__METHOD__,
'IGNORE' );
if ( $dbw->affectedRows() ) {
- wfDebug( __METHOD__.": set on ($field, $id)\n" );
+ wfDebug( __METHOD__ . ": set on ($field, $id)\n" );
return true;
} else {
- wfDebug( __METHOD__." already set ($field, $id)\n" );
+ wfDebug( __METHOD__ . " already set ($field, $id)\n" );
return false;
}
}
@@ -1571,10 +1665,10 @@ class User {
array( $field => $id ),
__METHOD__ );
if ( $dbw->affectedRows() ) {
- wfDebug( __METHOD__.": killed on ($field, $id)\n" );
+ wfDebug( __METHOD__ . ": killed on ($field, $id)\n" );
return true;
} else {
- wfDebug( __METHOD__.": already gone ($field, $id)\n" );
+ wfDebug( __METHOD__ . ": already gone ($field, $id)\n" );
return false;
}
}
@@ -1648,6 +1742,9 @@ class User {
* for reload on the next hit.
*/
function invalidateCache() {
+ if( wfReadOnly() ) {
+ return;
+ }
$this->load();
if( $this->mId ) {
$this->mTouched = self::newTouchedTimestamp();
@@ -1668,7 +1765,7 @@ class User {
*/
function validateCache( $timestamp ) {
$this->load();
- return ($timestamp >= $this->mTouched);
+ return ( $timestamp >= $this->mTouched );
}
/**
@@ -1702,10 +1799,11 @@ class User {
}
if( !$this->isValidPassword( $str ) ) {
- global $wgMinimalPasswordLength;
- throw new PasswordError( wfMsgExt( 'passwordtooshort', array( 'parsemag' ),
+ global $wgMinimalPasswordLength;
+ $valid = $this->getPasswordValidity( $str );
+ throw new PasswordError( wfMsgExt( $valid, array( 'parsemag' ),
$wgMinimalPasswordLength ) );
- }
+ }
}
if( !$wgAuth->setPassword( $this, $str ) ) {
@@ -1735,7 +1833,7 @@ class User {
$this->mNewpassword = '';
$this->mNewpassTime = null;
}
-
+
/**
* Get the user's current token.
* @return \string Token
@@ -1744,7 +1842,7 @@ class User {
$this->load();
return $this->mToken;
}
-
+
/**
* Set the random token (used for persistent authentication)
* Called from loadDefaults() among other places.
@@ -1768,7 +1866,7 @@ class User {
$this->mToken = $token;
}
}
-
+
/**
* Set the cookie password
*
@@ -1795,7 +1893,7 @@ class User {
}
/**
- * Has password reminder email been sent within the last
+ * Has password reminder email been sent within the last
* $wgPasswordReminderResendTime hours?
* @return \bool True or false
*/
@@ -1866,8 +1964,8 @@ class User {
* @see getBoolOption()
* @see getIntOption()
*/
- function getOption( $oname, $defaultOverride = '' ) {
- $this->load();
+ function getOption( $oname, $defaultOverride = null ) {
+ $this->loadOptions();
if ( is_null( $this->mOptions ) ) {
if($defaultOverride != '') {
@@ -1877,12 +1975,22 @@ class User {
}
if ( array_key_exists( $oname, $this->mOptions ) ) {
- return trim( $this->mOptions[$oname] );
+ return $this->mOptions[$oname];
} else {
return $defaultOverride;
}
}
-
+
+ /**
+ * Get all user's options
+ *
+ * @return array
+ */
+ public function getOptions() {
+ $this->loadOptions();
+ return $this->mOptions;
+ }
+
/**
* Get the user's current setting for a given option, as a boolean value.
*
@@ -1894,7 +2002,7 @@ class User {
return (bool)$this->getOption( $oname );
}
-
+
/**
* Get the user's current setting for a given option, as a boolean value.
*
@@ -1919,32 +2027,26 @@ class User {
*/
function setOption( $oname, $val ) {
$this->load();
- if ( is_null( $this->mOptions ) ) {
- $this->mOptions = User::getDefaultOptions();
- }
+ $this->loadOptions();
+
if ( $oname == 'skin' ) {
# Clear cached skin, so the new one displays immediately in Special:Preferences
unset( $this->mSkin );
}
- // Filter out any newlines that may have passed through input validation.
- // Newlines are used to separate items in the options blob.
- 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]) ) {
+ if( is_null( $val ) && isset( $wgDefaultUserOptions[$oname] ) ) {
$val = $wgDefaultUserOptions[$oname];
}
+
$this->mOptions[$oname] = $val;
}
-
+
/**
* Reset all options to the site defaults
- */
- function restoreOptions() {
+ */
+ function resetOptions() {
$this->mOptions = User::getDefaultOptions();
}
@@ -1999,6 +2101,7 @@ class User {
*/
function getEffectiveGroups( $recache = false ) {
if ( $recache || is_null( $this->mEffectiveGroups ) ) {
+ wfProfileIn( __METHOD__ );
$this->mEffectiveGroups = $this->getGroups();
$this->mEffectiveGroups[] = '*';
if( $this->getId() ) {
@@ -2012,6 +2115,7 @@ class User {
# Hook for additional groups
wfRunHooks( 'UserEffectiveGroups', array( &$this, &$this->mEffectiveGroups ) );
}
+ wfProfileOut( __METHOD__ );
}
return $this->mEffectiveGroups;
}
@@ -2021,10 +2125,10 @@ class User {
* @return \int User'e edit count
*/
function getEditCount() {
- if ($this->getId()) {
+ if( $this->getId() ) {
if ( !isset( $this->mEditCount ) ) {
/* Populate the count, if it has not been populated yet */
- $this->mEditCount = User::edits($this->mId);
+ $this->mEditCount = User::edits( $this->mId );
}
return $this->mEditCount;
} else {
@@ -2079,7 +2183,6 @@ class User {
$this->invalidateCache();
}
-
/**
* Get whether the user is logged in
* @return \bool True or false
@@ -2120,51 +2223,61 @@ class User {
if( !$wgUseRCPatrol && !$wgUseNPPatrol )
return false;
}
- # Use strict parameter to avoid matching numeric 0 accidentally inserted
+ # Use strict parameter to avoid matching numeric 0 accidentally inserted
# by misconfiguration: 0 == 'foo'
return in_array( $action, $this->getRights(), true );
}
/**
- * Check whether to enable recent changes patrol features for this user
- * @return \bool True or false
- */
+ * Check whether to enable recent changes patrol features for this user
+ * @return \bool True or false
+ */
public function useRCPatrol() {
global $wgUseRCPatrol;
- return( $wgUseRCPatrol && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
+ return( $wgUseRCPatrol && ( $this->isAllowed( 'patrol' ) || $this->isAllowed( 'patrolmarks' ) ) );
}
/**
- * Check whether to enable new pages patrol features for this user
- * @return \bool True or false
- */
+ * Check whether to enable new pages patrol features for this user
+ * @return \bool True or false
+ */
public function useNPPatrol() {
global $wgUseRCPatrol, $wgUseNPPatrol;
- return( ($wgUseRCPatrol || $wgUseNPPatrol) && ($this->isAllowed('patrol') || $this->isAllowed('patrolmarks')) );
+ return( ( $wgUseRCPatrol || $wgUseNPPatrol ) && ( $this->isAllowed( 'patrol' ) || $this->isAllowed( 'patrolmarks' ) ) );
}
/**
- * Get the current skin, loading it if required
- * @return \type{Skin} Current skin
+ * Get the current skin, loading it if required, and setting a title
+ * @param $t Title: the title to use in the skin
+ * @return Skin The current skin
* @todo FIXME : need to check the old failback system [AV]
*/
- function &getSkin() {
- global $wgRequest, $wgAllowUserSkin, $wgDefaultSkin;
- if ( ! isset( $this->mSkin ) ) {
+ function &getSkin( $t = null ) {
+ if ( !isset( $this->mSkin ) ) {
wfProfileIn( __METHOD__ );
- if( $wgAllowUserSkin ) {
+ global $wgHiddenPrefs;
+ if( !in_array( 'skin', $wgHiddenPrefs ) ) {
# get the user skin
+ global $wgRequest;
$userSkin = $this->getOption( 'skin' );
- $userSkin = $wgRequest->getVal('useskin', $userSkin);
+ $userSkin = $wgRequest->getVal( 'useskin', $userSkin );
} else {
# if we're not allowing users to override, then use the default
+ global $wgDefaultSkin;
$userSkin = $wgDefaultSkin;
}
-
+
$this->mSkin =& Skin::newFromKey( $userSkin );
wfProfileOut( __METHOD__ );
}
+ if( $t || !$this->mSkin->getTitle() ) {
+ if ( !$t ) {
+ global $wgOut;
+ $t = $wgOut->getTitle();
+ }
+ $this->mSkin->setTitle( $t );
+ }
return $this->mSkin;
}
@@ -2212,9 +2325,9 @@ class User {
return;
}
- if ($title->getNamespace() == NS_USER_TALK &&
+ if( $title->getNamespace() == NS_USER_TALK &&
$title->getText() == $this->getName() ) {
- if (!wfRunHooks('UserClearNewTalkNotification', array(&$this)))
+ if( !wfRunHooks( 'UserClearNewTalkNotification', array( &$this ) ) )
return;
$this->setNewtalk( false );
}
@@ -2232,8 +2345,8 @@ class User {
// The query to find out if it is watched is cached both in memcached and per-invocation,
// and when it does have to be executed, it can be on a slave
// If this is the user's newtalk page, we always update the timestamp
- if ($title->getNamespace() == NS_USER_TALK &&
- $title->getText() == $wgUser->getName())
+ if( $title->getNamespace() == NS_USER_TALK &&
+ $title->getText() == $wgUser->getName() )
{
$watched = true;
} elseif ( $this->getId() == $wgUser->getId() ) {
@@ -2248,7 +2361,7 @@ class User {
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'watchlist',
array( /* SET */
- 'wl_notificationtimestamp' => NULL
+ 'wl_notificationtimestamp' => null
), array( /* WHERE */
'wl_title' => $title->getDBkey(),
'wl_namespace' => $title->getNamespace(),
@@ -2275,7 +2388,7 @@ class User {
$dbw = wfGetDB( DB_MASTER );
$dbw->update( 'watchlist',
array( /* SET */
- 'wl_notificationtimestamp' => NULL
+ 'wl_notificationtimestamp' => null
), array( /* WHERE */
'wl_user' => $currentUser
), __METHOD__
@@ -2286,52 +2399,41 @@ class User {
}
/**
- * Encode this user's options as a string
- * @return \string Encoded options
- * @private
- */
- function encodeOptions() {
- $this->load();
- if ( is_null( $this->mOptions ) ) {
- $this->mOptions = User::getDefaultOptions();
- }
- $a = array();
- foreach ( $this->mOptions as $oname => $oval ) {
- array_push( $a, $oname.'='.$oval );
- }
- $s = implode( "\n", $a );
- return $s;
- }
-
- /**
* Set this user's options from an encoded string
* @param $str \string Encoded options to import
* @private
*/
function decodeOptions( $str ) {
+ if( !$str )
+ return;
+
+ $this->mOptionsLoaded = true;
+ $this->mOptionOverrides = array();
+
$this->mOptions = array();
$a = explode( "\n", $str );
foreach ( $a as $s ) {
$m = array();
if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
$this->mOptions[$m[1]] = $m[2];
+ $this->mOptionOverrides[$m[1]] = $m[2];
}
}
}
-
+
/**
- * Set a cookie on the user's client. Wrapper for
+ * Set a cookie on the user's client. Wrapper for
* WebResponse::setCookie
* @param $name \string Name of the cookie to set
* @param $value \string Value to set
- * @param $exp \int Expiration time, as a UNIX time value;
+ * @param $exp \int Expiration time, as a UNIX time value;
* if 0 or not specified, use the default $wgCookieExpiration
*/
- protected function setCookie( $name, $value, $exp=0 ) {
+ protected function setCookie( $name, $value, $exp = 0 ) {
global $wgRequest;
$wgRequest->response()->setcookie( $name, $value, $exp );
}
-
+
/**
* Clear a cookie on the user's client
* @param $name \string Name of the cookie to clear
@@ -2346,7 +2448,7 @@ class User {
function setCookies() {
$this->load();
if ( 0 == $this->mId ) return;
- $session = array(
+ $session = array(
'wsUserID' => $this->mId,
'wsToken' => $this->mToken,
'wsUserName' => $this->getName()
@@ -2360,9 +2462,9 @@ class User {
} else {
$cookies['Token'] = false;
}
-
+
wfRunHooks( 'UserSetCookies', array( $this, &$session, &$cookies ) );
- #check for null, since the hook could cause a null value
+ #check for null, since the hook could cause a null value
if ( !is_null( $session ) && isset( $_SESSION ) ){
$_SESSION = $session + $_SESSION;
}
@@ -2379,8 +2481,7 @@ class User {
* Log this user out.
*/
function logout() {
- global $wgUser;
- if( wfRunHooks( 'UserLogout', array(&$this) ) ) {
+ if( wfRunHooks( 'UserLogout', array( &$this ) ) ) {
$this->doLogout();
}
}
@@ -2423,8 +2524,8 @@ class User {
'user_real_name' => $this->mRealName,
'user_email' => $this->mEmail,
'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
- 'user_options' => $this->encodeOptions(),
- 'user_touched' => $dbw->timestamp($this->mTouched),
+ 'user_options' => '',
+ 'user_touched' => $dbw->timestamp( $this->mTouched ),
'user_token' => $this->mToken,
'user_email_token' => $this->mEmailToken,
'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
@@ -2432,6 +2533,9 @@ class User {
'user_id' => $this->mId
), __METHOD__
);
+
+ $this->saveOptions();
+
wfRunHooks( 'UserSaveSettings', array( $this ) );
$this->clearSharedCache();
$this->getUserPage()->invalidateCache();
@@ -2472,7 +2576,7 @@ class User {
$user = new User;
$user->load();
if ( isset( $params['options'] ) ) {
- $user->mOptions = $params['options'] + $user->mOptions;
+ $user->mOptions = $params['options'] + (array)$user->mOptions;
unset( $params['options'] );
}
$dbw = wfGetDB( DB_MASTER );
@@ -2486,7 +2590,7 @@ class User {
'user_email' => $user->mEmail,
'user_email_authenticated' => $dbw->timestampOrNull( $user->mEmailAuthenticated ),
'user_real_name' => $user->mRealName,
- 'user_options' => $user->encodeOptions(),
+ 'user_options' => '',
'user_token' => $user->mToken,
'user_registration' => $dbw->timestamp( $user->mRegistration ),
'user_editcount' => 0,
@@ -2520,7 +2624,7 @@ class User {
'user_email' => $this->mEmail,
'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ),
'user_real_name' => $this->mRealName,
- 'user_options' => $this->encodeOptions(),
+ 'user_options' => '',
'user_token' => $this->mToken,
'user_registration' => $dbw->timestamp( $this->mRegistration ),
'user_editcount' => 0,
@@ -2530,6 +2634,8 @@ class User {
// Clear instance cache other than user table data, which is already accurate
$this->clearInstanceCache();
+
+ $this->saveOptions();
}
/**
@@ -2537,7 +2643,7 @@ class User {
* they've successfully logged in from.
*/
function spreadBlock() {
- wfDebug( __METHOD__."()\n" );
+ wfDebug( __METHOD__ . "()\n" );
$this->load();
if ( $this->mId == 0 ) {
return;
@@ -2548,15 +2654,14 @@ class User {
return;
}
- $userblock->doAutoblock( wfGetIp() );
-
+ $userblock->doAutoblock( wfGetIP() );
}
/**
* Generate a string which will be different for any combination of
* user options which would produce different parser output.
* This will be used as part of the hash key for the parser cache,
- * so users will the same options can share the same cached data
+ * so users with the same options can share the same cached data
* safely.
*
* Extensions which require it should install 'PageRenderingHash' hook,
@@ -2579,7 +2684,7 @@ class User {
if ( $wgUseDynamicDates ) {
$confstr .= '!' . $this->getDatePreference();
}
- $confstr .= '!' . ($this->getOption( 'numberheadings' ) ? '1' : '');
+ $confstr .= '!' . ( $this->getOption( 'numberheadings' ) ? '1' : '' );
$confstr .= '!' . $wgLang->getCode();
$confstr .= '!' . $this->getOption( 'thumbsize' );
// add in language specific options, if any
@@ -2674,32 +2779,6 @@ 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
@@ -2838,14 +2917,16 @@ class User {
$url = $this->confirmationTokenUrl( $token );
$invalidateURL = $this->invalidationTokenUrl( $token );
$this->saveSettings();
-
+
return $this->sendMail( wfMsg( 'confirmemail_subject' ),
wfMsg( 'confirmemail_body',
wfGetIP(),
$this->getName(),
$url,
$wgLang->timeanddate( $expiration, false ),
- $invalidateURL ) );
+ $invalidateURL,
+ $wgLang->date( $expiration, false ),
+ $wgLang->time( $expiration, false ) ) );
}
/**
@@ -2901,6 +2982,7 @@ class User {
function confirmationTokenUrl( $token ) {
return $this->getTokenUrl( 'ConfirmEmail', $token );
}
+
/**
* Return a URL the user can use to invalidate their email address.
* @param $token \string Accepts the email confirmation token
@@ -2910,7 +2992,7 @@ class User {
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
@@ -2941,6 +3023,7 @@ class User {
*/
function confirmEmail() {
$this->setEmailAuthenticationTimestamp( wfTimestampNow() );
+ wfRunHooks( 'ConfirmEmailComplete', array( $this ) );
return true;
}
@@ -2955,6 +3038,7 @@ class User {
$this->mEmailToken = null;
$this->mEmailTokenExpires = null;
$this->setEmailAuthenticationTimestamp( null );
+ wfRunHooks( 'InvalidateEmailComplete', array( $this ) );
return true;
}
@@ -2975,7 +3059,7 @@ class User {
*/
function canSendEmail() {
global $wgEnableEmail, $wgEnableUserEmail;
- if( !$wgEnableEmail || !$wgEnableUserEmail ) {
+ if( !$wgEnableEmail || !$wgEnableUserEmail || !$this->isAllowed( 'sendemail' ) ) {
return false;
}
$canSend = $this->isEmailConfirmed();
@@ -3042,7 +3126,7 @@ class User {
? $this->mRegistration
: false;
}
-
+
/**
* Get the timestamp of the first edit
*
@@ -3059,7 +3143,7 @@ class User {
);
if( !$time ) return false; // no edits
return wfTimestamp( TS_MW, $time );
- }
+ }
/**
* Get the permissions associated with a given list of groups
@@ -3068,8 +3152,9 @@ class User {
* @return \type{\arrayof{\string}} List of permission key names for given groups combined
*/
static function getGroupPermissions( $groups ) {
- global $wgGroupPermissions;
+ global $wgGroupPermissions, $wgRevokePermissions;
$rights = array();
+ // grant every granted permission first
foreach( $groups as $group ) {
if( isset( $wgGroupPermissions[$group] ) ) {
$rights = array_merge( $rights,
@@ -3077,12 +3162,19 @@ class User {
array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
}
}
- return array_unique($rights);
+ // now revoke the revoked permissions
+ foreach( $groups as $group ) {
+ if( isset( $wgRevokePermissions[$group] ) ) {
+ $rights = array_diff( $rights,
+ array_keys( array_filter( $wgRevokePermissions[$group] ) ) );
+ }
+ }
+ return array_unique( $rights );
}
-
+
/**
* Get all the groups who have a given permission
- *
+ *
* @param $role \string Role to check
* @return \type{\arrayof{\string}} List of internal group names with the given permission
*/
@@ -3136,9 +3228,9 @@ class User {
* @return \type{\arrayof{\string}} Array of internal group names
*/
static function getAllGroups() {
- global $wgGroupPermissions;
+ global $wgGroupPermissions, $wgRevokePermissions;
return array_diff(
- array_keys( $wgGroupPermissions ),
+ array_merge( array_keys( $wgGroupPermissions ), array_keys( $wgRevokePermissions ) ),
self::getImplicitGroups()
);
}
@@ -3190,7 +3282,7 @@ class User {
}
/**
- * Create a link to the group in HTML, if available;
+ * Create a link to the group in HTML, if available;
* else return the group name.
*
* @param $group \string Internal name of the group
@@ -3205,14 +3297,14 @@ class User {
if( $title ) {
global $wgUser;
$sk = $wgUser->getSkin();
- return $sk->makeLinkObj( $title, htmlspecialchars( $text ) );
+ return $sk->link( $title, htmlspecialchars( $text ) );
} else {
return $text;
}
}
/**
- * Create a link to the group in Wikitext, if available;
+ * Create a link to the group in Wikitext, if available;
* else return the group name.
*
* @param $group \string Internal name of the group
@@ -3233,6 +3325,115 @@ class User {
}
/**
+ * 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 ),
+ * 'add-self' => array( addablegroups to self),
+ * 'remove-self' => array( removable groups from self) )
+ */
+ static function changeableByGroup( $group ) {
+ global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
+
+ $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
+ if( empty( $wgAddGroups[$group] ) ) {
+ // Don't add anything to $groups
+ } elseif( $wgAddGroups[$group] === true ) {
+ // You get everything
+ $groups['add'] = self::getAllGroups();
+ } elseif( is_array( $wgAddGroups[$group] ) ) {
+ $groups['add'] = $wgAddGroups[$group];
+ }
+
+ // Same thing for remove
+ if( empty( $wgRemoveGroups[$group] ) ) {
+ } elseif( $wgRemoveGroups[$group] === true ) {
+ $groups['remove'] = self::getAllGroups();
+ } elseif( is_array( $wgRemoveGroups[$group] ) ) {
+ $groups['remove'] = $wgRemoveGroups[$group];
+ }
+
+ // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
+ if( empty( $wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
+ foreach( $wgGroupsAddToSelf as $key => $value ) {
+ if( is_int( $key ) ) {
+ $wgGroupsAddToSelf['user'][] = $value;
+ }
+ }
+ }
+
+ if( empty( $wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
+ foreach( $wgGroupsRemoveFromSelf as $key => $value ) {
+ if( is_int( $key ) ) {
+ $wgGroupsRemoveFromSelf['user'][] = $value;
+ }
+ }
+ }
+
+ // Now figure out what groups the user can add to him/herself
+ if( empty( $wgGroupsAddToSelf[$group] ) ) {
+ } elseif( $wgGroupsAddToSelf[$group] === true ) {
+ // No idea WHY this would be used, but it's there
+ $groups['add-self'] = User::getAllGroups();
+ } elseif( is_array( $wgGroupsAddToSelf[$group] ) ) {
+ $groups['add-self'] = $wgGroupsAddToSelf[$group];
+ }
+
+ if( empty( $wgGroupsRemoveFromSelf[$group] ) ) {
+ } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
+ $groups['remove-self'] = User::getAllGroups();
+ } elseif( is_array( $wgGroupsRemoveFromSelf[$group] ) ) {
+ $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Returns an array of groups that this user can add and remove
+ * @return Array array( 'add' => array( addablegroups ),
+ * 'remove' => array( removablegroups ),
+ * 'add-self' => array( addablegroups to self),
+ * 'remove-self' => array( removable groups from self) )
+ */
+ function changeableGroups() {
+ if( $this->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' => array(),
+ 'remove-self' => array()
+ );
+ $addergroups = $this->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'] );
+ $groups['add-self'] = array_unique( $groups['add-self'] );
+ $groups['remove-self'] = array_unique( $groups['remove-self'] );
+ }
+ return $groups;
+ }
+
+ /**
* Increment the user's edit-count field.
* Will have no effect for anonymous users.
*/
@@ -3275,7 +3476,7 @@ class User {
// edit count in user cache too
$this->invalidateCache();
}
-
+
/**
* Get the description of a given right
*
@@ -3312,7 +3513,7 @@ class User {
* Make a new-style password hash
*
* @param $password \string Plain-text password
- * @param $salt \string Optional salt, may be random or the user ID.
+ * @param $salt \string Optional salt, may be random or the user ID.
* If unspecified or false, will generate one automatically
* @return \string Password hash
*/
@@ -3323,7 +3524,7 @@ class User {
if( !wfRunHooks( 'UserCryptPassword', array( &$password, &$salt, &$wgPasswordSalt, &$hash ) ) ) {
return $hash;
}
-
+
if( $wgPasswordSalt ) {
if ( $salt === false ) {
$salt = substr( wfGenerateToken(), 0, 8 );
@@ -3346,12 +3547,12 @@ class User {
static function comparePasswords( $hash, $password, $userId = false ) {
$m = false;
$type = substr( $hash, 0, 3 );
-
+
$result = false;
if( !wfRunHooks( 'UserComparePasswords', array( &$hash, &$password, &$userId, &$result ) ) ) {
return $result;
}
-
+
if ( $type == ':A:' ) {
# Unsalted
return md5( $password ) === substr( $hash, 3 );
@@ -3364,26 +3565,33 @@ class User {
return self::oldCrypt( $password, $userId ) === $hash;
}
}
-
+
/**
* Add a newuser log entry for this user
* @param $byEmail Boolean: account made by email?
*/
public function addNewUserLogEntry( $byEmail = false ) {
- global $wgUser, $wgContLang, $wgNewUserLog;
- if( empty($wgNewUserLog) ) {
+ global $wgUser, $wgNewUserLog;
+ if( empty( $wgNewUserLog ) ) {
return true; // disabled
}
- $talk = $wgContLang->getFormattedNsText( NS_TALK );
+
if( $this->getName() == $wgUser->getName() ) {
$action = 'create';
$message = '';
} else {
$action = 'create2';
- $message = $byEmail ? wfMsgForContent( 'newuserlog-byemail' ) : '';
+ $message = $byEmail
+ ? wfMsgForContent( 'newuserlog-byemail' )
+ : '';
}
$log = new LogPage( 'newusers' );
- $log->addEntry( $action, $this->getUserPage(), $message, array( $this->getId() ) );
+ $log->addEntry(
+ $action,
+ $this->getUserPage(),
+ $message,
+ array( $this->getId() )
+ );
return true;
}
@@ -3393,7 +3601,7 @@ class User {
*/
public function addNewUserLogEntryAutoCreate() {
global $wgNewUserLog;
- if( empty($wgNewUserLog) ) {
+ if( empty( $wgNewUserLog ) ) {
return true; // disabled
}
$log = new LogPage( 'newusers', false );
@@ -3401,4 +3609,131 @@ class User {
return true;
}
+ protected function loadOptions() {
+ $this->load();
+ if ( $this->mOptionsLoaded || !$this->getId() )
+ return;
+
+ $this->mOptions = self::getDefaultOptions();
+
+ // Maybe load from the object
+ if ( !is_null( $this->mOptionOverrides ) ) {
+ wfDebug( "Loading options for user " . $this->getId() . " from override cache.\n" );
+ foreach( $this->mOptionOverrides as $key => $value ) {
+ $this->mOptions[$key] = $value;
+ }
+ } else {
+ wfDebug( "Loading options for user " . $this->getId() . " from database.\n" );
+ // Load from database
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $res = $dbr->select(
+ 'user_properties',
+ '*',
+ array( 'up_user' => $this->getId() ),
+ __METHOD__
+ );
+
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $this->mOptionOverrides[$row->up_property] = $row->up_value;
+ $this->mOptions[$row->up_property] = $row->up_value;
+ }
+ }
+
+ $this->mOptionsLoaded = true;
+
+ wfRunHooks( 'UserLoadOptions', array( $this, &$this->mOptions ) );
+ }
+
+ protected function saveOptions() {
+ global $wgAllowPrefChange;
+
+ $extuser = ExternalUser::newFromUser( $this );
+
+ $this->loadOptions();
+ $dbw = wfGetDB( DB_MASTER );
+
+ $insert_rows = array();
+
+ $saveOptions = $this->mOptions;
+
+ // Allow hooks to abort, for instance to save to a global profile.
+ // Reset options to default state before saving.
+ if( !wfRunHooks( 'UserSaveOptions', array( $this, &$saveOptions ) ) )
+ return;
+
+ foreach( $saveOptions as $key => $value ) {
+ # Don't bother storing default values
+ if ( ( is_null( self::getDefaultOption( $key ) ) &&
+ !( $value === false || is_null($value) ) ) ||
+ $value != self::getDefaultOption( $key ) ) {
+ $insert_rows[] = array(
+ 'up_user' => $this->getId(),
+ 'up_property' => $key,
+ 'up_value' => $value,
+ );
+ }
+ if ( $extuser && isset( $wgAllowPrefChange[$key] ) ) {
+ switch ( $wgAllowPrefChange[$key] ) {
+ case 'local':
+ case 'message':
+ break;
+ case 'semiglobal':
+ case 'global':
+ $extuser->setPref( $key, $value );
+ }
+ }
+ }
+
+ $dbw->begin();
+ $dbw->delete( 'user_properties', array( 'up_user' => $this->getId() ), __METHOD__ );
+ $dbw->insert( 'user_properties', $insert_rows, __METHOD__ );
+ $dbw->commit();
+ }
+
+ /**
+ * Provide an array of HTML5 attributes to put on an input element
+ * intended for the user to enter a new password. This may include
+ * required, title, and/or pattern, depending on $wgMinimalPasswordLength.
+ *
+ * Do *not* use this when asking the user to enter his current password!
+ * Regardless of configuration, users may have invalid passwords for whatever
+ * reason (e.g., they were set before requirements were tightened up).
+ * Only use it when asking for a new password, like on account creation or
+ * ResetPass.
+ *
+ * Obviously, you still need to do server-side checking.
+ *
+ * @return array Array of HTML attributes suitable for feeding to
+ * Html::element(), directly or indirectly. (Don't feed to Xml::*()!
+ * That will potentially output invalid XHTML 1.0 Transitional, and will
+ * get confused by the boolean attribute syntax used.)
+ */
+ public static function passwordChangeInputAttribs() {
+ global $wgMinimalPasswordLength;
+
+ if ( $wgMinimalPasswordLength == 0 ) {
+ return array();
+ }
+
+ # Note that the pattern requirement will always be satisfied if the
+ # input is empty, so we need required in all cases.
+ $ret = array( 'required' );
+
+ # We can't actually do this right now, because Opera 9.6 will print out
+ # the entered password visibly in its error message! When other
+ # browsers add support for this attribute, or Opera fixes its support,
+ # we can add support with a version check to avoid doing this on Opera
+ # versions where it will be a problem. Reported to Opera as
+ # DSK-262266, but they don't have a public bug tracker for us to follow.
+ /*
+ if ( $wgMinimalPasswordLength > 1 ) {
+ $ret['pattern'] = '.{' . intval( $wgMinimalPasswordLength ) . ',}';
+ $ret['title'] = wfMsgExt( 'passwordtooshort', 'parsemag',
+ $wgMinimalPasswordLength );
+ }
+ */
+
+ return $ret;
+ }
}
diff --git a/includes/UserMailer.php b/includes/UserMailer.php
index b6484935..daf8b621 100644
--- a/includes/UserMailer.php
+++ b/includes/UserMailer.php
@@ -188,11 +188,12 @@ class UserMailer {
$headers .= "{$endl}Reply-To: " . $replyto->toString();
}
+ wfDebug( "Sending mail via internal mail() function\n" );
+
$wgErrorString = '';
$html_errors = ini_get( 'html_errors' );
ini_set( 'html_errors', '0' );
set_error_handler( array( 'UserMailer', 'errorHandler' ) );
- wfDebug( "Sending mail via internal mail() function\n" );
if (function_exists('mail')) {
if (is_array($to)) {
@@ -263,9 +264,9 @@ class UserMailer {
*
*/
class EmailNotification {
- private $to, $subject, $body, $replyto, $from;
- private $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor;
- private $mailTargets = array();
+ protected $to, $subject, $body, $replyto, $from;
+ protected $user, $title, $timestamp, $summary, $minorEdit, $oldid, $composed_common, $editor;
+ protected $mailTargets = array();
/**
* Send emails corresponding to the user $editor editing the page $title.
@@ -471,6 +472,7 @@ class EmailNotification {
$keys['$PAGEMINOREDIT'] = $medit;
$keys['$PAGESUMMARY'] = $summary;
+ $keys['$UNWATCHURL'] = $this->title->getFullUrl( 'action=unwatch' );
$subject = strtr( $subject, $keys );
@@ -572,8 +574,13 @@ class EmailNotification {
# $PAGEEDITDATE is the time and date of the page change
# expressed in terms of individual local time of the notification
# recipient, i.e. watching user
- $body = str_replace('$PAGEEDITDATE',
- $wgContLang->timeanddate( $this->timestamp, true, false, $timecorrection ),
+ $body = str_replace(
+ array( '$PAGEEDITDATEANDTIME',
+ '$PAGEEDITDATE',
+ '$PAGEEDITTIME' ),
+ array( $wgContLang->timeanddate( $this->timestamp, true, false, $timecorrection ),
+ $wgContLang->date( $this->timestamp, true, false, $timecorrection ),
+ $wgContLang->time( $this->timestamp, true, false, $timecorrection ) ),
$body);
return UserMailer::send($to, $this->from, $this->subject, $body, $this->replyto);
diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php
index 8a65a01a..0d6b8151 100644
--- a/includes/UserRightsProxy.php
+++ b/includes/UserRightsProxy.php
@@ -1,11 +1,21 @@
<?php
-
/**
* Cut-down copy of User interface for local-interwiki-database
* user rights manipulation.
*/
class UserRightsProxy {
+
+ /**
+ * Constructor.
+ *
+ * @see newFromId()
+ * @see newFromName()
+ * @param $db DatabaseBase: db connection
+ * @param $database String: database name
+ * @param $name String: user name
+ * @param $id Integer: user ID
+ */
private function __construct( $db, $database, $name, $id ) {
$this->db = $db;
$this->database = $database;
@@ -14,14 +24,32 @@ class UserRightsProxy {
}
/**
+ * Accessor for $this->database
+ *
+ * @return String: database name
+ */
+ public function getDBName() {
+ return $this->database;
+ }
+
+ /**
* Confirm the selected database name is a valid local interwiki database name.
- * @return bool
+ *
+ * @param $database String: database name
+ * @return Boolean
*/
public static function validDatabase( $database ) {
global $wgLocalDatabases;
return in_array( $database, $wgLocalDatabases );
}
+ /**
+ * Same as User::whoIs()
+ *
+ * @param $database String: database name
+ * @param $id Integer: user ID
+ * @return String: user name or false if the user doesn't exist
+ */
public static function whoIs( $database, $id ) {
$user = self::newFromId( $database, $id );
if( $user ) {
@@ -33,12 +61,22 @@ class UserRightsProxy {
/**
* Factory function; get a remote user entry by ID number.
+ *
+ * @param $database String: database name
+ * @param $id Integer: user ID
* @return UserRightsProxy or null if doesn't exist
*/
public static function newFromId( $database, $id ) {
return self::newFromLookup( $database, 'user_id', intval( $id ) );
}
+ /**
+ * Factory function; get a remote user entry by name.
+ *
+ * @param $database String: database name
+ * @param $name String: user name
+ * @return UserRightsProxy or null if doesn't exist
+ */
public static function newFromName( $database, $name ) {
return self::newFromLookup( $database, 'user_name', $name );
}
@@ -62,8 +100,9 @@ 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 $database string
- * @return Database or null if invalid selection
+ *
+ * @param $database String
+ * @return DatabaseBase or null if invalid selection
*/
public static function getDB( $database ) {
global $wgLocalDatabases, $wgDBname;
@@ -86,15 +125,27 @@ class UserRightsProxy {
return $this->getId() == 0;
}
+ /**
+ * Same as User::getName()
+ *
+ * @return String
+ */
public function getName() {
return $this->name . '@' . $this->database;
}
+ /**
+ * Same as User::getUserPage()
+ *
+ * @return Title object
+ */
public function getUserPage() {
return Title::makeTitle( NS_USER, $this->getName() );
}
- // Replaces getUserGroups()
+ /**
+ * Replaces User::getUserGroups()
+ */
function getGroups() {
$res = $this->db->select( 'user_groups',
array( 'ug_group' ),
@@ -107,7 +158,9 @@ class UserRightsProxy {
return $groups;
}
- // replaces addUserGroup
+ /**
+ * Replaces User::addUserGroup()
+ */
function addGroup( $group ) {
$this->db->insert( 'user_groups',
array(
@@ -118,7 +171,9 @@ class UserRightsProxy {
array( 'IGNORE' ) );
}
- // replaces removeUserGroup
+ /**
+ * Replaces User::removeUserGroup()
+ */
function removeGroup( $group ) {
$this->db->delete( 'user_groups',
array(
@@ -128,7 +183,9 @@ class UserRightsProxy {
__METHOD__ );
}
- // replaces touchUser
+ /**
+ * Replaces User::touchUser()
+ */
function invalidateCache() {
$this->db->update( 'user',
array( 'user_touched' => $this->db->timestamp() ),
diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php
index a2c1f036..d1c15296 100644
--- a/includes/WatchedItem.php
+++ b/includes/WatchedItem.php
@@ -62,7 +62,7 @@ class WatchedItem {
'wl_user' => $this->id,
'wl_namespace' => MWNamespace::getSubject($this->ns),
'wl_title' => $this->ti,
- 'wl_notificationtimestamp' => NULL
+ 'wl_notificationtimestamp' => null
), __METHOD__, 'IGNORE' );
// Every single watched page needs now to be listed in watchlist;
@@ -72,7 +72,7 @@ class WatchedItem {
'wl_user' => $this->id,
'wl_namespace' => MWNamespace::getTalk($this->ns),
'wl_title' => $this->ti,
- 'wl_notificationtimestamp' => NULL
+ 'wl_notificationtimestamp' => null
), __METHOD__, 'IGNORE' );
wfProfileOut( __METHOD__ );
diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php
index 82f62f6a..e9e79ee1 100644
--- a/includes/WatchlistEditor.php
+++ b/includes/WatchlistEditor.php
@@ -143,8 +143,8 @@ class WatchlistEditor {
if( !$title instanceof Title )
$title = Title::newFromText( $title );
if( $title instanceof Title ) {
- $output->addHTML( "<li>" . $skin->makeLinkObj( $title )
- . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")</li>\n" );
+ $output->addHTML( "<li>" . $skin->link( $title )
+ . ' (' . $skin->link( $title->getTalkPage(), $talk ) . ")</li>\n" );
}
}
$output->addHTML( "</ul>\n" );
@@ -340,7 +340,7 @@ class WatchlistEditor {
if( ( $count = $this->showItemCount( $output, $user ) ) > 0 ) {
$self = SpecialPage::getTitleFor( 'Watchlist' );
$form = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $self->getLocalUrl( 'action=edit' ) ) );
+ 'action' => $self->getLocalUrl( array( 'action' => 'edit' ) ) ) );
$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
$form .= "<fieldset>\n<legend>" . wfMsgHtml( 'watchlistedit-normal-legend' ) . "</legend>";
$form .= wfMsgExt( 'watchlistedit-normal-explain', 'parse' );
@@ -409,15 +409,27 @@ class WatchlistEditor {
private function buildRemoveLine( $title, $redirect, $skin ) {
global $wgLang;
- $link = $skin->makeLinkObj( $title );
+ $link = $skin->link( $title );
if( $redirect )
$link = '<span class="watchlistredir">' . $link . '</span>';
- $tools[] = $skin->makeLinkObj( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
+ $tools[] = $skin->link( $title->getTalkPage(), wfMsgHtml( 'talkpagelinktext' ) );
if( $title->exists() ) {
- $tools[] = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'history_short' ), 'action=history' );
+ $tools[] = $skin->link(
+ $title,
+ wfMsgHtml( 'history_short' ),
+ array(),
+ array( 'action' => 'history' ),
+ array( 'known', 'noclasses' )
+ );
}
if( $title->getNamespace() == NS_USER && !$title->isSubpage() ) {
- $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $title->getText() ), wfMsgHtml( 'contributions' ) );
+ $tools[] = $skin->link(
+ SpecialPage::getTitleFor( 'Contributions', $title->getText() ),
+ wfMsgHtml( 'contributions' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
return "<li>"
. Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) )
@@ -435,7 +447,7 @@ class WatchlistEditor {
$this->showItemCount( $output, $user );
$self = SpecialPage::getTitleFor( 'Watchlist' );
$form = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $self->getLocalUrl( 'action=raw' ) ) );
+ 'action' => $self->getLocalUrl( array( 'action' => 'raw' ) ) ) );
$form .= Xml::hidden( 'token', $wgUser->editToken( 'watchlistedit' ) );
$form .= '<fieldset><legend>' . wfMsgHtml( 'watchlistedit-raw-legend' ) . '</legend>';
$form .= wfMsgExt( 'watchlistedit-raw-explain', 'parse' );
@@ -487,7 +499,14 @@ class WatchlistEditor {
$tools = array();
$modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' );
foreach( $modes as $mode => $subpage ) {
- $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Watchlist', $subpage ), wfMsgHtml( "watchlisttools-{$mode}" ) );
+ // can use messages 'watchlisttools-view', 'watchlisttools-edit', 'watchlisttools-raw'
+ $tools[] = $skin->link(
+ SpecialPage::getTitleFor( 'Watchlist', $subpage ),
+ wfMsgHtml( "watchlisttools-{$mode}" ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
}
return $wgLang->pipeList( $tools );
}
diff --git a/includes/WebRequest.php b/includes/WebRequest.php
index 0928e4d5..b6d6d27a 100644
--- a/includes/WebRequest.php
+++ b/includes/WebRequest.php
@@ -39,16 +39,15 @@ if ( !function_exists( '__autoload' ) ) {
* not create a second WebRequest object; make a FauxRequest object if
* you want to pass arbitrary data to some function in place of the web
* input.
- *
+ *
* @ingroup HTTP
*/
class WebRequest {
- var $data = array();
- var $headers;
+ protected $data, $headers = array();
private $_response;
- function __construct() {
- /// @fixme This preemptive de-quoting can interfere with other web libraries
+ public function __construct() {
+ /// @todo 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();
@@ -65,13 +64,15 @@ class WebRequest {
* as we may need the list of language variants to determine
* available variant URLs.
*/
- function interpolateTitle() {
+ public function interpolateTitle() {
global $wgUsePathInfo;
+
if ( $wgUsePathInfo ) {
// PATH_INFO is mangled due to http://bugs.php.net/bug.php?id=31892
// And also by Apache 2.x, double slashes are converted to single slashes.
// So we will use REQUEST_URI if possible.
$matches = array();
+
if ( !empty( $_SERVER['REQUEST_URI'] ) ) {
// Slurp out the path portion to examine...
$url = $_SERVER['REQUEST_URI'];
@@ -161,9 +162,8 @@ class WebRequest {
* used for undoing the evil that is magic_quotes_gpc.
* @param $arr array: will be modified
* @return array the original array
- * @private
*/
- function &fix_magic_quotes( &$arr ) {
+ private function &fix_magic_quotes( &$arr ) {
foreach( $arr as $key => $val ) {
if( is_array( $val ) ) {
$this->fix_magic_quotes( $arr[$key] );
@@ -179,10 +179,11 @@ class WebRequest {
* through fix_magic_quotes to strip out the stupid slashes.
* WARNING: This should only be done once! Running a second
* time could damage the values.
- * @private
*/
- function checkMagicQuotes() {
- if ( function_exists( 'get_magic_quotes_gpc' ) && get_magic_quotes_gpc() ) {
+ private function checkMagicQuotes() {
+ $mustFixQuotes = function_exists( 'get_magic_quotes_gpc' )
+ && get_magic_quotes_gpc();
+ if( $mustFixQuotes ) {
$this->fix_magic_quotes( $_COOKIE );
$this->fix_magic_quotes( $_ENV );
$this->fix_magic_quotes( $_GET );
@@ -204,7 +205,8 @@ class WebRequest {
$data[$key] = $this->normalizeUnicode( $val );
}
} else {
- $data = UtfNormal::cleanUp( $data );
+ global $wgContLang;
+ $data = $wgContLang->normalize( $data );
}
return $data;
}
@@ -216,9 +218,12 @@ class WebRequest {
* @param $name string
* @param $default mixed
* @return mixed
- * @private
*/
- function getGPCVal( $arr, $name, $default ) {
+ private function getGPCVal( $arr, $name, $default ) {
+ # PHP is so nice to not touch input data, except sometimes:
+ # http://us2.php.net/variables.external#language.variables.external.dot-in-names
+ # Work around PHP *feature* to avoid *bugs* elsewhere.
+ $name = strtr( $name, '.', '_' );
if( isset( $arr[$name] ) ) {
global $wgContLang;
$data = $arr[$name];
@@ -246,7 +251,7 @@ class WebRequest {
* @param $default string: optional default (or NULL)
* @return string
*/
- function getVal( $name, $default = NULL ) {
+ public function getVal( $name, $default = null ) {
$val = $this->getGPCVal( $this->data, $name, $default );
if( is_array( $val ) ) {
$val = $default;
@@ -257,14 +262,14 @@ class WebRequest {
return (string)$val;
}
}
-
+
/**
* Set an aribtrary value into our get/post data.
* @param $key string Key name to use
* @param $value mixed Value to set
* @return mixed old value if one was present, null otherwise
*/
- function setVal( $key, $value ) {
+ public function setVal( $key, $value ) {
$ret = isset( $this->data[$key] ) ? $this->data[$key] : null;
$this->data[$key] = $value;
return $ret;
@@ -279,7 +284,7 @@ class WebRequest {
* @param $default array: optional default (or NULL)
* @return array
*/
- function getArray( $name, $default = NULL ) {
+ public function getArray( $name, $default = null ) {
$val = $this->getGPCVal( $this->data, $name, $default );
if( is_null( $val ) ) {
return null;
@@ -298,7 +303,7 @@ class WebRequest {
* @param $default array: option default (or NULL)
* @return array of ints
*/
- function getIntArray( $name, $default = NULL ) {
+ public function getIntArray( $name, $default = null ) {
$val = $this->getArray( $name, $default );
if( is_array( $val ) ) {
$val = array_map( 'intval', $val );
@@ -314,7 +319,7 @@ class WebRequest {
* @param $default int
* @return int
*/
- function getInt( $name, $default = 0 ) {
+ public function getInt( $name, $default = 0 ) {
return intval( $this->getVal( $name, $default ) );
}
@@ -325,7 +330,7 @@ class WebRequest {
* @param $name string
* @return int
*/
- function getIntOrNull( $name ) {
+ public function getIntOrNull( $name ) {
$val = $this->getVal( $name );
return is_numeric( $val )
? intval( $val )
@@ -340,7 +345,7 @@ class WebRequest {
* @param $default bool
* @return bool
*/
- function getBool( $name, $default = false ) {
+ public function getBool( $name, $default = false ) {
return $this->getVal( $name, $default ) ? true : false;
}
@@ -351,10 +356,10 @@ class WebRequest {
* @param $name string
* @return bool
*/
- function getCheck( $name ) {
+ public function getCheck( $name ) {
# Checkboxes and buttons are only present when clicked
# Presence connotes truth, abscense false
- $val = $this->getVal( $name, NULL );
+ $val = $this->getVal( $name, null );
return isset( $val );
}
@@ -370,7 +375,7 @@ class WebRequest {
* @param $default string: optional
* @return string
*/
- function getText( $name, $default = '' ) {
+ public function getText( $name, $default = '' ) {
global $wgContLang;
$val = $this->getVal( $name, $default );
return str_replace( "\r\n", "\n",
@@ -382,7 +387,7 @@ class WebRequest {
* If no arguments are given, returns all input values.
* No transformation is performed on the values.
*/
- function getValues() {
+ public function getValues() {
$names = func_get_args();
if ( count( $names ) == 0 ) {
$names = array_keys( $this->data );
@@ -407,7 +412,7 @@ class WebRequest {
*
* @return bool
*/
- function wasPosted() {
+ public function wasPosted() {
return $_SERVER['REQUEST_METHOD'] == 'POST';
}
@@ -422,7 +427,7 @@ class WebRequest {
*
* @return bool
*/
- function checkSessionCookie() {
+ public function checkSessionCookie() {
return isset( $_COOKIE[session_name()] );
}
@@ -430,8 +435,8 @@ class WebRequest {
* Return the path portion of the request URI.
* @return string
*/
- function getRequestURL() {
- if( isset( $_SERVER['REQUEST_URI'] ) ) {
+ public function getRequestURL() {
+ if( isset( $_SERVER['REQUEST_URI']) && strlen($_SERVER['REQUEST_URI']) ) {
$base = $_SERVER['REQUEST_URI'];
} elseif( isset( $_SERVER['SCRIPT_NAME'] ) ) {
// Probably IIS; doesn't set REQUEST_URI
@@ -465,7 +470,7 @@ class WebRequest {
* Return the request URI with the canonical service and hostname.
* @return string
*/
- function getFullRequestURL() {
+ public function getFullRequestURL() {
global $wgServer;
return $wgServer . $this->getRequestURL();
}
@@ -475,7 +480,7 @@ class WebRequest {
* @param $query String: query string fragment; do not include initial '?'
* @return string
*/
- function appendQuery( $query ) {
+ public function appendQuery( $query ) {
global $wgTitle;
$basequery = '';
foreach( $_GET as $var => $val ) {
@@ -500,11 +505,11 @@ class WebRequest {
* @param $query String: query string fragment; do not include initial '?'
* @return string
*/
- function escapeAppendQuery( $query ) {
+ public function escapeAppendQuery( $query ) {
return htmlspecialchars( $this->appendQuery( $query ) );
}
- function appendQueryValue( $key, $value, $onlyquery = false ) {
+ public function appendQueryValue( $key, $value, $onlyquery = false ) {
return $this->appendQueryArray( array( $key => $value ), $onlyquery );
}
@@ -515,7 +520,7 @@ class WebRequest {
* the complete URL
* @return string
*/
- function appendQueryArray( $array, $onlyquery = false ) {
+ public function appendQueryArray( $array, $onlyquery = false ) {
global $wgTitle;
$newquery = $_GET;
unset( $newquery['title'] );
@@ -533,7 +538,7 @@ class WebRequest {
* @param $optionname String: to specify an option other than rclimit to pull from.
* @return array first element is limit, second is offset
*/
- function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
+ public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) {
global $wgUser;
$limit = $this->getInt( 'limit', 0 );
@@ -555,9 +560,9 @@ class WebRequest {
* @param $key String:
* @return string or NULL if no such file.
*/
- function getFileTempname( $key ) {
+ public function getFileTempname( $key ) {
if( !isset( $_FILES[$key] ) ) {
- return NULL;
+ return null;
}
return $_FILES[$key]['tmp_name'];
}
@@ -567,7 +572,7 @@ class WebRequest {
* @param $key String:
* @return integer
*/
- function getFileSize( $key ) {
+ public function getFileSize( $key ) {
if( !isset( $_FILES[$key] ) ) {
return 0;
}
@@ -579,7 +584,7 @@ class WebRequest {
* @param $key String:
* @return integer
*/
- function getUploadError( $key ) {
+ public function getUploadError( $key ) {
if( !isset( $_FILES[$key] ) || !isset( $_FILES[$key]['error'] ) ) {
return 0/*UPLOAD_ERR_OK*/;
}
@@ -597,16 +602,17 @@ class WebRequest {
* @param $key String:
* @return string or NULL if no such file.
*/
- function getFileName( $key ) {
+ public function getFileName( $key ) {
+ global $wgContLang;
if( !isset( $_FILES[$key] ) ) {
- return NULL;
+ return null;
}
$name = $_FILES[$key]['name'];
# Safari sends filenames in HTML-encoded Unicode form D...
# Horrid and evil! Let's try to make some kind of sense of it.
$name = Sanitizer::decodeCharReferences( $name );
- $name = UtfNormal::cleanUp( $name );
+ $name = $wgContLang->normalize( $name );
wfDebug( "WebRequest::getFileName() '" . $_FILES[$key]['name'] . "' normalized to '$name'\n" );
return $name;
}
@@ -615,10 +621,11 @@ class WebRequest {
* Return a handle to WebResponse style object, for setting cookies,
* headers and other stuff, for Request being worked on.
*/
- function response() {
+ public function response() {
/* Lazy initialization of response object for this request */
- if (!is_object($this->_response)) {
- $this->_response = new WebResponse;
+ if ( !is_object( $this->_response ) ) {
+ $class = ( $this instanceof FauxRequest ) ? 'FauxResponse' : 'WebResponse';
+ $this->_response = new $class();
}
return $this->_response;
}
@@ -627,11 +634,10 @@ class WebRequest {
* Get a request header, or false if it isn't set
* @param $name String: case-insensitive header name
*/
- function getHeader( $name ) {
+ public function getHeader( $name ) {
$name = strtoupper( $name );
if ( function_exists( 'apache_request_headers' ) ) {
- if ( !isset( $this->headers ) ) {
- $this->headers = array();
+ if ( !$this->headers ) {
foreach ( apache_request_headers() as $tempName => $tempValue ) {
$this->headers[ strtoupper( $tempName ) ] = $tempValue;
}
@@ -650,18 +656,53 @@ class WebRequest {
}
}
}
-
+
/*
* Get data from $_SESSION
+ * @param $key String Name of key in $_SESSION
+ * @return mixed
*/
- function getSessionData( $key ) {
+ public function getSessionData( $key ) {
if( !isset( $_SESSION[$key] ) )
return null;
return $_SESSION[$key];
}
- function setSessionData( $key, $data ) {
+
+ /**
+ * Set session data
+ * @param $key String Name of key in $_SESSION
+ * @param $data mixed
+ */
+ public function setSessionData( $key, $data ) {
$_SESSION[$key] = $data;
}
+
+ /**
+ * Returns true if the PATH_INFO ends with an extension other than a script
+ * extension. This could confuse IE for scripts that send arbitrary data which
+ * is not HTML but may be detected as such.
+ *
+ * Various past attempts to use the URL to make this check have generally
+ * run up against the fact that CGI does not provide a standard method to
+ * determine the URL. PATH_INFO may be mangled (e.g. if cgi.fix_pathinfo=0),
+ * but only by prefixing it with the script name and maybe some other stuff,
+ * the extension is not mangled. So this should be a reasonably portable
+ * way to perform this security check.
+ */
+ public function isPathInfoBad() {
+ global $wgScriptExtension;
+
+ if ( !isset( $_SERVER['PATH_INFO'] ) ) {
+ return false;
+ }
+ $pi = $_SERVER['PATH_INFO'];
+ $dotPos = strrpos( $pi, '.' );
+ if ( $dotPos === false ) {
+ return false;
+ }
+ $ext = substr( $pi, $dotPos );
+ return !in_array( $ext, array( $wgScriptExtension, '.php', '.php5' ) );
+ }
}
/**
@@ -670,64 +711,73 @@ class WebRequest {
* @ingroup HTTP
*/
class FauxRequest extends WebRequest {
- var $wasPosted = false;
+ private $wasPosted = false;
+ private $session = array();
+ private $response;
/**
* @param $data Array of *non*-urlencoded key => value pairs, the
* fake GET/POST values
* @param $wasPosted Bool: whether to treat the data as POST
*/
- function FauxRequest( $data, $wasPosted = false, $session = null ) {
+ public function __construct( $data, $wasPosted = false, $session = null ) {
if( is_array( $data ) ) {
$this->data = $data;
} else {
throw new MWException( "FauxRequest() got bogus data" );
}
$this->wasPosted = $wasPosted;
- $this->headers = array();
- $this->session = $session ? $session : array();
+ if( $session )
+ $this->session = $session;
}
-
- function notImplemented( $method ) {
+
+ private function notImplemented( $method ) {
throw new MWException( "{$method}() not implemented" );
}
- function getText( $name, $default = '' ) {
+ public function getText( $name, $default = '' ) {
# Override; don't recode since we're using internal data
return (string)$this->getVal( $name, $default );
}
- function getValues() {
+ public function getValues() {
return $this->data;
}
- function wasPosted() {
+ public function wasPosted() {
return $this->wasPosted;
}
- function checkSessionCookie() {
+ public function checkSessionCookie() {
return false;
}
- function getRequestURL() {
+ public function getRequestURL() {
$this->notImplemented( __METHOD__ );
}
- function appendQuery( $query ) {
+ public function appendQuery( $query ) {
$this->notImplemented( __METHOD__ );
}
- function getHeader( $name ) {
+ public function getHeader( $name ) {
return isset( $this->headers[$name] ) ? $this->headers[$name] : false;
}
- function getSessionData( $key ) {
- if( !isset( $this->session[$key] ) )
- return null;
- return $this->session[$key];
+ public function setHeader( $name, $val ) {
+ $this->headers[$name] = $val;
+ }
+
+ public function getSessionData( $key ) {
+ if( isset( $this->session[$key] ) )
+ return $this->session[$key];
}
- function setSessionData( $key, $data ) {
+
+ public function setSessionData( $key, $data ) {
$this->notImplemented( __METHOD__ );
}
+ public function isPathInfoBad() {
+ return false;
+ }
}
diff --git a/includes/WebResponse.php b/includes/WebResponse.php
index 09d37385..f7d57e41 100644
--- a/includes/WebResponse.php
+++ b/includes/WebResponse.php
@@ -6,7 +6,7 @@
*/
class WebResponse {
- /**
+ /**
* Output a HTTP header, wrapper for PHP's
* header()
* @param $string String: header to output
@@ -58,3 +58,31 @@ class WebResponse {
}
}
}
+
+
+class FauxResponse extends WebResponse {
+ private $headers;
+ private $cookies;
+
+ public function header($string, $replace=true) {
+ list($key, $val) = explode(":", $string, 2);
+
+ if($replace || !isset($this->headers[$key])) {
+ $this->headers[$key] = $val;
+ }
+ }
+
+ public function getheader($key) {
+ return $this->headers[$key];
+ }
+
+ public function setcookie( $name, $value, $expire = 0 ) {
+ $this->cookies[$name] = $value;
+ }
+
+ public function getcookie( $name ) {
+ if ( isset($this->cookies[$name]) ) {
+ return $this->cookies[$name];
+ }
+ }
+} \ No newline at end of file
diff --git a/includes/WebStart.php b/includes/WebStart.php
index edc58cb3..d62b4a62 100644
--- a/includes/WebStart.php
+++ b/includes/WebStart.php
@@ -46,7 +46,6 @@ if ( function_exists ( 'getrusage' ) ) {
$wgRUstart = array();
}
unset( $IP );
-@ini_set( 'allow_url_fopen', 0 ); # For security
# Valid web server entry point, enable includes.
# Please don't move this line to includes/Defines.php. This line essentially
@@ -66,7 +65,11 @@ if ( $IP === false ) {
# Start profiler
-require_once( "$IP/StartProfiler.php" );
+if( file_exists("$IP/StartProfiler.php") ) {
+ require_once( "$IP/StartProfiler.php" );
+} else {
+ require_once( "$IP/includes/ProfilerStub.php" );
+}
wfProfileIn( 'WebStart.php-conf' );
# Load up some global defines.
diff --git a/includes/Wiki.php b/includes/Wiki.php
index 38f19c96..dc4467b6 100644
--- a/includes/Wiki.php
+++ b/includes/Wiki.php
@@ -42,7 +42,6 @@ class MediaWiki {
/**
* Initialization of ... everything
* Performs the request too
- * FIXME: why is this crap called "initialize" when it performs everything?
*
* @param $title Title ($wgTitle)
* @param $article Article
@@ -50,14 +49,22 @@ class MediaWiki {
* @param $user User
* @param $request WebRequest
*/
- function initialize( &$title, &$article, &$output, &$user, $request ) {
+ function performRequestForTitle( &$title, &$article, &$output, &$user, $request ) {
wfProfileIn( __METHOD__ );
+
+ $output->setTitle( $title );
+
+ wfRunHooks( 'BeforeInitialize', array( &$title, &$article, &$output, &$user, $request, $this ) );
+
if( !$this->preliminaryChecks( $title, $output, $request ) ) {
wfProfileOut( __METHOD__ );
return;
}
- if( !$this->initializeSpecialCases( $title, $output, $request ) ) {
- $new_article = $this->initializeArticle( $title, $request );
+ // Call handleSpecialCases() to deal with all special requests...
+ if( !$this->handleSpecialCases( $title, $output, $request ) ) {
+ // ...otherwise treat it as an article view. The article
+ // may be a redirect to another article or URL.
+ $new_article = $this->initializeArticle( $title, $output, $request );
if( is_object( $new_article ) ) {
$article = $new_article;
$this->performAction( $output, $article, $title, $user, $request );
@@ -102,11 +109,11 @@ class MediaWiki {
if( $wgRequest->getVal( 'printable' ) === 'yes' ) {
$wgOut->setPrintable();
}
- $ret = NULL;
+ $ret = null;
if( $curid = $wgRequest->getInt( 'curid' ) ) {
# URLs like this are generated by RC, because rc_title isn't always accurate
$ret = Title::newFromID( $curid );
- } elseif( '' == $title && 'delete' != $action ) {
+ } elseif( $title == '' && $action != 'delete' ) {
$ret = Title::newMainPage();
} else {
$ret = Title::newFromURL( $title );
@@ -149,8 +156,9 @@ class MediaWiki {
# the Read array in order for the user to see it. (We have to check here to
# catch special pages etc. We check again in Article::view())
if( !is_null( $title ) && !$title->userCanRead() ) {
+ global $wgDeferredUpdateList;
$output->loginToUse();
- $output->output();
+ $this->finalCleanup( $wgDeferredUpdateList, $output );
$output->disable();
return false;
}
@@ -164,39 +172,54 @@ class MediaWiki {
* - redirect loop
* - special pages
*
- * FIXME: why is this crap called "initialize" when it performs everything?
- *
* @param $title Title
* @param $output OutputPage
* @param $request WebRequest
* @return bool true if the request is already executed
*/
- function initializeSpecialCases( &$title, &$output, $request ) {
+ function handleSpecialCases( &$title, &$output, $request ) {
wfProfileIn( __METHOD__ );
-
+ global $wgContLang, $wgUser;
$action = $this->getVal( 'Action' );
- if( is_null($title) || $title->getDBkey() == '' ) {
+ $perferred = $wgContLang->getPreferredVariant( false );
+
+ // Invalid titles. Bug 21776: The interwikis must redirect even if the page name is empty.
+ if( is_null($title) || ( ($title->getDBkey() == '') && ($title->getInterwiki() == '') ) ) {
$title = SpecialPage::getTitleFor( 'Badtitle' );
# Die now before we mess up $wgArticle and the skin stops working
throw new ErrorPageError( 'badtitle', 'badtitletext' );
+
+ // Interwiki redirects
} else if( $title->getInterwiki() != '' ) {
if( $rdfrom = $request->getVal( 'rdfrom' ) ) {
$url = $title->getFullURL( 'rdfrom=' . urlencode( $rdfrom ) );
} else {
- $url = $title->getFullURL();
+ $query = $request->getValues();
+ unset( $query['title'] );
+ $url = $title->getFullURL( $query );
}
/* Check for a redirect loop */
if( !preg_match( '/^' . preg_quote( $this->getVal('Server'), '/' ) . '/', $url ) && $title->isLocal() ) {
$output->redirect( $url );
} else {
$title = SpecialPage::getTitleFor( 'Badtitle' );
+ wfProfileOut( __METHOD__ );
throw new ErrorPageError( 'badtitle', 'badtitletext' );
}
+ // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant
} else if( $action == 'view' && !$request->wasPosted() &&
- ( !isset($this->GET['title']) || $title->getPrefixedDBKey() != $this->GET['title'] ) &&
+ ( ( !isset($this->GET['title']) || $title->getPrefixedDBKey() != $this->GET['title'] ) ||
+ // No valid variant in URL (if the main-language has multi-variants), to ensure
+ // anonymous access would always be redirect to a URL with 'variant' parameter
+ ( !isset($this->GET['variant']) && $wgContLang->hasVariants() && !$wgUser->isLoggedIn() ) ) &&
!count( array_diff( array_keys( $this->GET ), array( 'action', 'title' ) ) ) )
{
- $targetUrl = $title->getFullURL();
+ if( !$wgUser->isLoggedIn() ) {
+ $pref = $wgContLang->getPreferredVariant( false, $fromHeader = true );
+ $targetUrl = $title->getFullURL( '', $variant = $pref );
+ }
+ else
+ $targetUrl = $title->getFullURL();
// Redirect to canonical url, make it a 301 to allow caching
if( $targetUrl == $request->getFullRequestURL() ) {
$message = "Redirect loop detected!\n\n" .
@@ -219,11 +242,13 @@ class MediaWiki {
"to true.";
}
wfHttpError( 500, "Internal error", $message );
+ wfProfileOut( __METHOD__ );
return false;
} else {
$output->setSquidMaxage( 1200 );
$output->redirect( $targetUrl, '301' );
}
+ // Special pages
} else if( NS_SPECIAL == $title->getNamespace() ) {
/* actions that need to be made when we have a special pages */
SpecialPage::executePath( $title );
@@ -270,10 +295,11 @@ class MediaWiki {
* Create an Article object for the page, following redirects if needed.
*
* @param $title Title ($wgTitle)
- * @param $request WebRequest
+ * @param $output OutputPage ($wgOut)
+ * @param $request WebRequest ($wgRequest)
* @return mixed an Article, or a string to redirect to another URL
*/
- function initializeArticle( &$title, $request ) {
+ function initializeArticle( &$title, &$output, $request ) {
wfProfileIn( __METHOD__ );
$action = $this->getVal( 'action', 'view' );
@@ -302,13 +328,15 @@ class MediaWiki {
wfRunHooks( 'InitializeArticleMaybeRedirect',
array(&$title,&$request,&$ignoreRedirect,&$target,&$article) );
- // Follow redirects only for... redirects
- if( !$ignoreRedirect && $article->isRedirect() ) {
+ // Follow redirects only for... redirects.
+ // If $target is set, then a hook wanted to redirect.
+ if( !$ignoreRedirect && ($target || $article->isRedirect()) ) {
# Is the target already set by an extension?
$target = $target ? $target : $article->followRedirect();
if( is_string( $target ) ) {
if( !$this->getVal( 'DisableHardRedirects' ) ) {
// we'll need to redirect
+ wfProfileOut( __METHOD__ );
return $target;
}
}
@@ -320,6 +348,7 @@ class MediaWiki {
$rarticle->setRedirectedFrom( $title );
$article = $rarticle;
$title = $target;
+ $output->setTitle( $title );
}
}
} else {
@@ -331,14 +360,16 @@ class MediaWiki {
}
/**
- * Cleaning up by doing deferred updates, calling LBFactory and doing the output
+ * Cleaning up request by doing:
+ ** deferred updates, DB transaction, and the output
*
* @param $deferredUpdates array of updates to do
* @param $output OutputPage
*/
function finalCleanup( &$deferredUpdates, &$output ) {
wfProfileIn( __METHOD__ );
- # Now commit any transactions, so that unreported errors after output() don't roll back the whole thing
+ # Now commit any transactions, so that unreported errors after
+ # output() don't roll back the whole DB transaction
$factory = wfGetLBFactory();
$factory->commitMasterChanges();
# Output everything!
@@ -346,8 +377,6 @@ class MediaWiki {
# Do any deferred jobs
$this->doUpdates( $deferredUpdates );
$this->doJobs();
- # Commit and close up!
- $factory->shutdown();
wfProfileOut( __METHOD__ );
}
@@ -418,6 +447,10 @@ class MediaWiki {
*/
function restInPeace() {
wfLogProfilingData();
+ # Commit and close up!
+ $factory = wfGetLBFactory();
+ $factory->commitMasterChanges();
+ $factory->shutdown();
wfDebug( "Request ended normally\n" );
}
@@ -444,6 +477,16 @@ class MediaWiki {
$action = 'nosuchaction';
}
+ # Workaround for bug #20966: inability of IE to provide an action dependent
+ # on which submit button is clicked.
+ if ( $action === 'historysubmit' ) {
+ if ( $request->getBool( 'revisiondelete' ) ) {
+ $action = 'revisiondelete';
+ } else {
+ $action = 'view';
+ }
+ }
+
switch( $action ) {
case 'view':
$output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
@@ -519,9 +562,14 @@ class MediaWiki {
if( $request->getFullRequestURL() == $title->getInternalURL( 'action=history' ) ) {
$output->setSquidMaxage( $this->getVal( 'SquidMaxage' ) );
}
- $history = new PageHistory( $article );
+ $history = new HistoryPage( $article );
$history->history();
break;
+ case 'revisiondelete':
+ # For show/hide submission from history page
+ $special = SpecialPage::getPage( 'Revisiondelete' );
+ $special->execute( '' );
+ break;
default:
if( wfRunHooks( 'UnknownAction', array( $action, $article ) ) ) {
$output->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
diff --git a/includes/WikiMap.php b/includes/WikiMap.php
new file mode 100644
index 00000000..878e165f
--- /dev/null
+++ b/includes/WikiMap.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * Helper tools for dealing with other locally-hosted wikis
+ */
+class WikiMap {
+
+ /**
+ * Get a WikiReference object for $wikiID
+ *
+ * @param $wikiID String: wiki'd id (generally database name)
+ * @return WikiReference object or null if the wiki was not found
+ */
+ public static function getWiki( $wikiID ) {
+ global $wgConf, $IP;
+
+ $wgConf->loadFullData();
+
+ list( $major, $minor ) = $wgConf->siteFromDB( $wikiID );
+ if( isset( $major ) ) {
+ $server = $wgConf->get( 'wgServer', $wikiID, $major,
+ array( 'lang' => $minor, 'site' => $major ) );
+ $path = $wgConf->get( 'wgArticlePath', $wikiID, $major,
+ array( 'lang' => $minor, 'site' => $major ) );
+ return new WikiReference( $major, $minor, $server, $path );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Convenience to get the wiki's display name
+ *
+ * @todo We can give more info than just the wiki id!
+ * @param $wikiID String: wiki'd id (generally database name)
+ * @return Wiki's name or $wiki_id if the wiki was not found
+ */
+ public static function getWikiName( $wikiID ) {
+ $wiki = WikiMap::getWiki( $wikiID );
+
+ if ( $wiki ) {
+ return $wiki->getDisplayName();
+ }
+ return $wikiID;
+ }
+
+ /**
+ * Convenience to get a link to a user page on a foreign wiki
+ *
+ * @param $wikiID String: wiki'd id (generally database name)
+ * @param $user String: user name (must be normalised before calling this function!)
+ * @param $text String: link's text; optional, default to "User:$user"
+ * @return String: HTML link or false if the wiki was not found
+ */
+ public static function foreignUserLink( $wikiID, $user, $text=null ) {
+ return self::makeForeignLink( $wikiID, "User:$user", $text );
+ }
+
+ /**
+ * Convenience to get a link to a page on a foreign wiki
+ *
+ * @param $wikiID String: wiki'd id (generally database name)
+ * @param $page String: page name (must be normalised before calling this function!)
+ * @param $text String: link's text; optional, default to $page
+ * @return String: HTML link or false if the wiki was not found
+ */
+ public static function makeForeignLink( $wikiID, $page, $text=null ) {
+ global $wgUser;
+ $sk = $wgUser->getSkin();
+
+ if ( !$text )
+ $text = $page;
+
+ $url = self::getForeignURL( $wikiID, $page );
+ if ( $url === false )
+ return false;
+
+ return $sk->makeExternalLink( $url, $text );
+ }
+
+ /**
+ * Convenience to get a url to a page on a foreign wiki
+ *
+ * @param $wikiID String: wiki'd id (generally database name)
+ * @param $page String: page name (must be normalised before calling this function!)
+ * @return String: URL or false if the wiki was not found
+ */
+ public static function getForeignURL( $wikiID, $page ) {
+ $wiki = WikiMap::getWiki( $wikiID );
+
+ if ( $wiki )
+ return $wiki->getUrl( $page );
+
+ return false;
+ }
+}
+
+/**
+ * Reference to a locally-hosted wiki
+ */
+class WikiReference {
+ private $mMinor; ///< 'en', 'meta', 'mediawiki', etc
+ private $mMajor; ///< 'wiki', 'wiktionary', etc
+ private $mServer; ///< server override, 'www.mediawiki.org'
+ private $mPath; ///< path override, '/wiki/$1'
+
+ public function __construct( $major, $minor, $server, $path ) {
+ $this->mMajor = $major;
+ $this->mMinor = $minor;
+ $this->mServer = $server;
+ $this->mPath = $path;
+ }
+
+ public function getHostname() {
+ $prefixes = array( 'http://', 'https://' );
+ foreach ( $prefixes as $prefix ) {
+ if ( substr( $this->mServer, 0, strlen( $prefix ) ) ) {
+ return substr( $this->mServer, strlen( $prefix ) );
+ }
+ }
+ throw new MWException( "Invalid hostname for wiki {$this->mMinor}.{$this->mMajor}" );
+ }
+
+ /**
+ * Get the the URL in a way to de displayed to the user
+ * More or less Wikimedia specific
+ *
+ * @return String
+ */
+ public function getDisplayName() {
+ $url = $this->getUrl( '' );
+ $url = preg_replace( '!^https?://!', '', $url );
+ $url = preg_replace( '!/index\.php(\?title=|/)$!', '/', $url );
+ $url = preg_replace( '!/wiki/$!', '/', $url );
+ $url = preg_replace( '!/$!', '', $url );
+ return $url;
+ }
+
+ /**
+ * Helper function for getUrl()
+ *
+ * @todo FIXME: this may be generalized...
+ * @param $page String: page name (must be normalised before calling this function!)
+ * @return String: Url fragment
+ */
+ private function getLocalUrl( $page ) {
+ return str_replace( '$1', wfUrlEncode( str_replace( ' ', '_', $page ) ), $this->mPath );
+ }
+
+ /**
+ * Get a URL to a page on this foreign wiki
+ *
+ * @param $page String: page name (must be normalised before calling this function!)
+ * @return String: Url
+ */
+ public function getUrl( $page ) {
+ return
+ $this->mServer .
+ $this->getLocalUrl( $page );
+ }
+}
diff --git a/includes/Xml.php b/includes/Xml.php
index bbe0717c..464b142c 100644
--- a/includes/Xml.php
+++ b/includes/Xml.php
@@ -56,7 +56,7 @@ class Xml {
/**
* Format an XML element as with self::element(), but run text through the
- * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8
+ * $wgContLang->normalize() validator first to ensure that no invalid UTF-8
* is passed.
*
* @param $element String:
@@ -65,12 +65,13 @@ class Xml {
* @return string
*/
public static function elementClean( $element, $attribs = array(), $contents = '') {
+ global $wgContLang;
if( $attribs ) {
$attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
}
if( $contents ) {
wfProfileIn( __METHOD__ . '-norm' );
- $contents = UtfNormal::cleanUp( $contents );
+ $contents = $wgContLang->normalize( $contents );
wfProfileOut( __METHOD__ . '-norm' );
}
return self::element( $element, $attribs, $contents );
@@ -162,12 +163,12 @@ class Xml {
public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) {
global $wgLang;
$options = array();
- if( is_null( $selected ) )
+ if( is_null( $selected ) )
$selected = '';
- if( !is_null( $allmonths ) )
+ 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 );
+ $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' );
@@ -394,21 +395,14 @@ class Xml {
* @return string HTML
*/
public static function submitButton( $value, $attribs=array() ) {
- return self::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
+ return Html::element( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
}
/**
- * Convenience function to build an HTML hidden form field.
- * @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
+ * @deprecated Synonymous to Html::hidden()
*/
- public static function hidden( $name, $value, $attribs=array() ) {
- return self::element( 'input', array(
- 'name' => $name,
- 'type' => 'hidden',
- 'value' => $value ) + $attribs );
+ public static function hidden( $name, $value, $attribs = array() ) {
+ return Html::hidden( $name, $value, $attribs );
}
/**
@@ -574,7 +568,10 @@ class Xml {
$s = 'null';
} elseif ( is_int( $value ) ) {
$s = $value;
- } elseif ( is_array( $value ) ) {
+ } elseif ( is_array( $value ) && // Make sure it's not associative.
+ array_keys($value) === range( 0, count($value) - 1 ) ||
+ count($value) == 0
+ ) {
$s = '[';
foreach ( $value as $elt ) {
if ( $s != '[' ) {
@@ -583,7 +580,8 @@ class Xml {
$s .= self::encodeJsVar( $elt );
}
$s .= ']';
- } elseif ( is_object( $value ) ) {
+ } elseif ( is_object( $value ) || is_array( $value ) ) {
+ // Objects and associative arrays
$s = '{';
foreach ( (array)$value as $name => $elt ) {
if ( $s != '{' ) {
@@ -692,9 +690,9 @@ class Xml {
/**
* Build a table of data
- * @param array $rows An array of arrays of strings, each to be a row in a table
- * @param array $attribs Attributes to apply to the table tag [optional]
- * @param array $headers An array of strings to use as table headers [optional]
+ * @param $rows An array of arrays of strings, each to be a row in a table
+ * @param $attribs An array of attributes to apply to the table tag [optional]
+ * @param $headers An array of strings to use as table headers [optional]
* @return string
*/
public static function buildTable( $rows, $attribs = array(), $headers = null ) {
@@ -717,7 +715,8 @@ class Xml {
/**
* Build a row for a table
- * @param array $cells An array of strings to put in <td>
+ * @param $attribs An array of attributes to apply to the tr tag
+ * @param $cells An array of strings to put in <td>
* @return string
*/
public static function buildTableRow( $attribs, $cells ) {
@@ -751,11 +750,43 @@ class XmlSelect {
$this->attributes[$name] = $value;
}
+ public function getAttribute( $name ) {
+ if ( isset($this->attributes[$name]) ) {
+ return $this->attributes[$name];
+ } else {
+ return null;
+ }
+ }
+
public function addOption( $name, $value = false ) {
// Stab stab stab
$value = ($value !== false) ? $value : $name;
$this->options[] = Xml::option( $name, $value, $value === $this->default );
}
+
+ // This accepts an array of form
+ // label => value
+ // label => ( label => value, label => value )
+ public function addOptions( $options ) {
+ $this->options[] = trim(self::formatOptions( $options, $this->default ));
+ }
+
+ // This accepts an array of form
+ // label => value
+ // label => ( label => value, label => value )
+ static function formatOptions( $options, $default = false ) {
+ $data = '';
+ foreach( $options as $label => $value ) {
+ if ( is_array( $value ) ) {
+ $contents = self::formatOptions( $value, $default );
+ $data .= Xml::tags( 'optgroup', array( 'label' => $label ), $contents ) . "\n";
+ } else {
+ $data .= Xml::option( $label, $value, $value === $default ) . "\n";
+ }
+ }
+
+ return $data;
+ }
public function getHTML() {
return Xml::tags( 'select', $this->attributes, implode( "\n", $this->options ) );
diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php
index d7655df0..400cdd2e 100644
--- a/includes/ZhConversion.php
+++ b/includes/ZhConversion.php
@@ -109,6 +109,7 @@ $zh2Hant = array(
'买' => '買',
'乱' => '亂',
'争' => '爭',
+'于' => '於',
'亏' => '虧',
'云' => '雲',
'亚' => '亞',
@@ -243,6 +244,7 @@ $zh2Hant = array(
'卤' => '鹵',
'卫' => '衛',
'却' => '卻',
+'卺' => '巹',
'厂' => '廠',
'厅' => '廳',
'历' => '歷',
@@ -481,6 +483,7 @@ $zh2Hant = array(
'帻' => '幘',
'帼' => '幗',
'幂' => '冪',
+'幞' => '襆',
'并' => '並',
'广' => '廣',
'庆' => '慶',
@@ -1246,6 +1249,7 @@ $zh2Hant = array(
'罴' => '羆',
'羁' => '羈',
'羟' => '羥',
+'羡' => '羨',
'翘' => '翹',
'耢' => '耮',
'耧' => '耬',
@@ -1314,7 +1318,7 @@ $zh2Hant = array(
'苍' => '蒼',
'苎' => '苧',
'苏' => '蘇',
-'苧' => '苎',
+'苧' => '薴',
'苹' => '蘋',
'茎' => '莖',
'茏' => '蘢',
@@ -1413,6 +1417,7 @@ $zh2Hant = array(
'蝇' => '蠅',
'蝈' => '蟈',
'蝉' => '蟬',
+'蝎' => '蠍',
'蝼' => '螻',
'蝾' => '蠑',
'螀' => '螿',
@@ -1710,6 +1715,7 @@ $zh2Hant = array(
'躏' => '躪',
'躜' => '躦',
'躯' => '軀',
+'軿' => '𫚒',
'车' => '車',
'轧' => '軋',
'轨' => '軌',
@@ -1816,6 +1822,7 @@ $zh2Hant = array(
'鉴' => '鑒',
'銮' => '鑾',
'錾' => '鏨',
+'鎭' => '鎮',
'钅' => '釒',
'钆' => '釓',
'钇' => '釔',
@@ -2143,6 +2150,7 @@ $zh2Hant = array(
'鞑' => '韃',
'鞒' => '鞽',
'鞯' => '韉',
+'鞲' => '韝',
'韦' => '韋',
'韧' => '韌',
'韨' => '韍',
@@ -2517,6 +2525,7 @@ $zh2Hant = array(
'鹫' => '鷲',
'鹬' => '鷸',
'鹭' => '鷺',
+'鹮' => '䴉',
'鹯' => '鸇',
'鹰' => '鷹',
'鹱' => '鸌',
@@ -2536,6 +2545,7 @@ $zh2Hant = array(
'鼍' => '鼉',
'鼗' => '鞀',
'鼹' => '鼴',
+'齄' => '齇',
'齐' => '齊',
'齑' => '齏',
'齿' => '齒',
@@ -2556,6 +2566,7 @@ $zh2Hant = array(
'龚' => '龔',
'龛' => '龕',
'龟' => '龜',
+'' => '棡',
'𠮶' => '嗰',
'𡒄' => '壈',
'𦈖' => '䌈',
@@ -2687,29 +2698,87 @@ $zh2Hant = array(
'𪎌' => '麳',
'𪚏' => '𪘀',
'𪚐' => '𪘯',
-'' => '棡',
+'𪞝' => '凙',
+'𪡏' => '嗹',
+'𪢮' => '圞',
+'𪨊' => '㞞',
+'𪨗' => '屩',
+'𪻐' => '瑽',
+'𪾢' => '睍',
+'𫁡' => '鴗',
+'𫂈' => '䉬',
+'𫄨' => '絺',
+'𫄸' => '纁',
+'𫌀' => '襀',
+'𫌨' => '覼',
+'𫍙' => '訑',
+'𫍟' => '𧦧',
+'𫍢' => '譊',
+'𫍰' => '諰',
+'𫍲' => '謏',
+'𫏋' => '蹻',
+'𫐄' => '軏',
+'𫐆' => '轣',
+'𫐉' => '軨',
+'𫐐' => '輗',
+'𫐓' => '輮',
+'𫓧' => '鈇',
+'𫓩' => '鏦',
+'𫔎' => '鐍',
+'𫗠' => '餦',
+'𫗦' => '餔',
+'𫗧' => '餗',
+'𫗮' => '餭',
+'𫗴' => '饘',
+'𫘝' => '駃',
+'𫘣' => '駻',
+'𫘤' => '騃',
+'𫘨' => '騠',
+'𫚈' => '鱮',
+'𫚉' => '魟',
+'𫚒' => '鮄',
+'𫚔' => '鮰',
+'𫚕' => '鰤',
+'𫚙' => '鯆',
+'𫛛' => '鳷',
+'𫛞' => '鴃',
+'𫛢' => '鸋',
+'𫛶' => '鶒',
+'𫛸' => '鶗',
'0多只' => '0多隻',
'0天后' => '0天後',
'0只' => '0隻',
'0余' => '0餘',
'1天后' => '1天後',
'1只' => '1隻',
+'1余' => '1餘',
'2天后' => '2天後',
'2只' => '2隻',
+'2余' => '2餘',
'3天后' => '3天後',
'3只' => '3隻',
+'3余' => '3餘',
'4天后' => '4天後',
'4只' => '4隻',
+'4余' => '4餘',
'5天后' => '5天後',
'5只' => '5隻',
+'5余' => '5餘',
'6天后' => '6天後',
'6只' => '6隻',
+'6余' => '6餘',
'7天后' => '7天後',
'7只' => '7隻',
+'7余' => '7餘',
'8天后' => '8天後',
'8只' => '8隻',
+'8余' => '8餘',
'9天后' => '9天後',
'9只' => '9隻',
+'9余' => '9餘',
+'·范' => '·范',
+'、克制' => '、剋制',
+'。克制' => '。剋制',
'〇只' => '〇隻',
'〇余' => '〇餘',
'一干二净' => '一乾二淨',
@@ -2717,15 +2786,22 @@ $zh2Hant = array(
'一伙头' => '一伙頭',
'一伙食' => '一伙食',
'一并' => '一併',
+'一个' => '一個',
'一个准' => '一個準',
+'一出刊' => '一出刊',
+'一出口' => '一出口',
+'一出版' => '一出版',
+'一出生' => '一出生',
+'一出祁山' => '一出祁山',
+'一出逃' => '一出逃',
'一前一后' => '一前一後',
'一划' => '一劃',
'一半只' => '一半只',
-'一口钟' => '一口鐘',
'一吊钱' => '一吊錢',
'一地里' => '一地裡',
'一伙' => '一夥',
'一天后' => '一天後',
+'一天钟' => '一天鐘',
'一干人' => '一干人',
'一干家中' => '一干家中',
'一干弟兄' => '一干弟兄',
@@ -2744,17 +2820,33 @@ $zh2Hant = array(
'一锅面' => '一鍋麵',
'一只' => '一隻',
'一面食' => '一面食',
+'一余' => '一餘',
'一发千钧' => '一髮千鈞',
'一哄而散' => '一鬨而散',
'丁丁当当' => '丁丁當當',
'丁丑' => '丁丑',
+'七个' => '七個',
+'七出刊' => '七出刊',
+'七出口' => '七出口',
+'七出版' => '七出版',
+'七出生' => '七出生',
+'七出祁山' => '七出祁山',
+'七出逃' => '七出逃',
'七划' => '七劃',
'七天后' => '七天後',
'七情六欲' => '七情六慾',
'七扎' => '七紮',
'七只' => '七隻',
+'七余' => '七餘',
'万俟' => '万俟',
'万旗' => '万旗',
+'三个' => '三個',
+'三出刊' => '三出刊',
+'三出口' => '三出口',
+'三出版' => '三出版',
+'三出生' => '三出生',
+'三出祁山' => '三出祁山',
+'三出逃' => '三出逃',
'三天后' => '三天後',
'三征七辟' => '三徵七辟',
'三准' => '三準',
@@ -2764,8 +2856,6 @@ $zh2Hant = array(
'三复' => '三複',
'三只' => '三隻',
'三余' => '三餘',
-'上吊自杀' => '上吊自殺',
-'上吊' => '上弔',
'上梁山' => '上梁山',
'上梁' => '上樑',
'上签名' => '上簽名',
@@ -2774,13 +2864,19 @@ $zh2Hant = array(
'上签收' => '上簽收',
'上签' => '上籤',
'上药' => '上藥',
+'上课钟' => '上課鐘',
'上面糊' => '上面糊',
'下仑路' => '下崙路',
'下于' => '下於',
'下梁' => '下樑',
'下注解' => '下注解',
+'下签名' => '下簽名',
+'下签字' => '下簽字',
+'下签写' => '下簽寫',
+'下签收' => '下簽收',
'下签' => '下籤',
'下药' => '下藥',
+'下课钟' => '下課鐘',
'不干不净' => '不乾不淨',
'不占' => '不佔',
'不克自制' => '不克自制',
@@ -2793,6 +2889,7 @@ $zh2Hant = array(
'不准翻印' => '不准翻印',
'不准许' => '不准許',
'不准谁' => '不准誰',
+'不克制' => '不剋制',
'不前不后' => '不前不後',
'不加自制' => '不加自制',
'不占凶吉' => '不占凶吉',
@@ -2848,18 +2945,25 @@ $zh2Hant = array(
'且于' => '且於',
'世田谷' => '世田谷',
'世界杯' => '世界盃',
+'世界里' => '世界裡',
+'世纪钟' => '世紀鐘',
+'世纪钟表' => '世紀鐘錶',
'丢丑' => '丟醜',
'并不准' => '並不准',
'并存着' => '並存著',
-'并于' => '並於',
+'并曰入淀' => '並曰入澱',
'并发动' => '並發動',
'并发展' => '並發展',
'并发现' => '並發現',
'并发表' => '並發表',
'中国国际信托投资公司' => '中國國際信托投資公司',
-'中国烟草总公司' => '中國烟草總公司',
+'中型钟' => '中型鐘',
+'中型钟表面' => '中型鐘表面',
+'中型钟表' => '中型鐘錶',
+'中型钟面' => '中型鐘面',
'中仑' => '中崙',
'中岳' => '中嶽',
+'中文里' => '中文裡',
'中于' => '中於',
'中签' => '中籤',
'中美发表' => '中美發表',
@@ -2873,8 +2977,8 @@ $zh2Hant = array(
'丰度' => '丰度',
'丰情' => '丰情',
'丰标' => '丰標',
-'丰标不凡' => '丰標不凡',
'丰標不凡' => '丰標不凡',
+'丰标不凡' => '丰標不凡',
'丰神' => '丰神',
'丰茸' => '丰茸',
'丰采' => '丰采',
@@ -2884,22 +2988,34 @@ $zh2Hant = array(
'丹药' => '丹藥',
'主仆' => '主僕',
'主干' => '主幹',
+'主钟差' => '主鐘差',
+'主钟曲线' => '主鐘曲線',
'么么小丑' => '么麼小丑',
'之一只' => '之一只',
'之二只' => '之二只',
'之八九只' => '之八九只',
'之后' => '之後',
'之征' => '之徵',
-'之于' => '之於',
'之托' => '之託',
+'之钟' => '之鐘',
'之余' => '之餘',
'乙丑' => '乙丑',
'九世之仇' => '九世之讎',
+'九个' => '九個',
+'九出刊' => '九出刊',
+'九出口' => '九出口',
+'九出版' => '九出版',
+'九出生' => '九出生',
+'九出祁山' => '九出祁山',
+'九出逃' => '九出逃',
'九划' => '九劃',
'九天后' => '九天後',
'九谷' => '九穀',
'九扎' => '九紮',
'九只' => '九隻',
+'九余' => '九餘',
+'九龙表行' => '九龍表行',
+'也克制' => '也剋制',
'也斗了胆' => '也斗了膽',
'干干' => '乾乾',
'干干儿的' => '乾乾兒的',
@@ -3040,6 +3156,7 @@ $zh2Hant = array(
'干酪' => '乾酪',
'干酵母' => '乾酵母',
'干醋' => '乾醋',
+'干重' => '乾重',
'干量' => '乾量',
'干阿奶' => '乾阿奶',
'干隆' => '乾隆',
@@ -3058,12 +3175,20 @@ $zh2Hant = array(
'乱发' => '亂髮',
'乱哄' => '亂鬨',
'乱哄不过来' => '亂鬨不過來',
+'了克制' => '了剋制',
'事后' => '事後',
'事情干脆' => '事情干脆',
'事有斗巧' => '事有鬥巧',
'事迹' => '事迹',
'事都干脆' => '事都干脆',
'二不棱登' => '二不稜登',
+'二个' => '二個',
+'二出刊' => '二出刊',
+'二出口' => '二出口',
+'二出版' => '二出版',
+'二出生' => '二出生',
+'二出祁山' => '二出祁山',
+'二出逃' => '二出逃',
'二划' => '二劃',
'二只得' => '二只得',
'二天后' => '二天後',
@@ -3074,14 +3199,181 @@ $zh2Hant = array(
'二里头' => '二里頭',
'二里頭' => '二里頭',
'二只' => '二隻',
+'二余' => '二餘',
+'于丹' => '于丹',
+'于于' => '于于',
+'于仁泰' => '于仁泰',
+'于佳卉' => '于佳卉',
+'于伟国' => '于偉國',
+'于偉國' => '于偉國',
+'于光远' => '于光遠',
+'于光遠' => '于光遠',
+'于克-蘭多縣' => '于克-蘭多縣',
+'于克-兰多县' => '于克-蘭多縣',
+'于克勒' => '于克勒',
+'于冕' => '于冕',
+'于凌奎' => '于凌奎',
+'于勒' => '于勒',
+'于化虎' => '于化虎',
+'于占元' => '于占元',
+'于台煙' => '于台煙',
+'于台烟' => '于台煙',
+'于右任' => '于右任',
+'于吉' => '于吉',
+'于品海' => '于品海',
+'于国桢' => '于國楨',
+'于國楨' => '于國楨',
+'于坚' => '于堅',
+'于堅' => '于堅',
+'于大寶' => '于大寶',
+'于大宝' => '于大寶',
+'于天仁' => '于天仁',
+'于奇库杜克' => '于奇庫杜克',
+'于奇庫杜克' => '于奇庫杜克',
+'于姓' => '于姓',
+'于娜' => '于娜',
+'于娟' => '于娟',
+'于子千' => '于子千',
+'于孔兼' => '于孔兼',
+'于學忠' => '于學忠',
+'于学忠' => '于學忠',
+'于家堡' => '于家堡',
+'于寘' => '于寘',
+'于小伟' => '于小偉',
+'于小偉' => '于小偉',
+'于小彤' => '于小彤',
+'于山' => '于山',
+'于山国' => '于山國',
+'于山國' => '于山國',
+'于帥' => '于帥',
+'于帅' => '于帥',
+'于幼軍' => '于幼軍',
+'于幼军' => '于幼軍',
+'于康震' => '于康震',
+'于廣洲' => '于廣洲',
+'于广洲' => '于廣洲',
+'于式枚' => '于式枚',
+'于從濂' => '于從濂',
+'于从濂' => '于從濂',
+'于德海' => '于德海',
+'于志宁' => '于志寧',
+'于志寧' => '于志寧',
+'于思' => '于思',
+'于慎行' => '于慎行',
+'于慧' => '于慧',
+'于成龙' => '于成龍',
+'于成龍' => '于成龍',
+'于振' => '于振',
+'于振武' => '于振武',
+'于敏' => '于敏',
+'于敏中' => '于敏中',
+'于斌' => '于斌',
+'于斯塔德' => '于斯塔德',
+'于斯納爾斯貝里' => '于斯納爾斯貝里',
+'于斯纳尔斯贝里' => '于斯納爾斯貝里',
+'于斯达尔' => '于斯達爾',
+'于斯達爾' => '于斯達爾',
+'于明涛' => '于明濤',
+'于明濤' => '于明濤',
+'于是之' => '于是之',
+'于晨楠' => '于晨楠',
+'于晴' => '于晴',
+'于會泳' => '于會泳',
+'于会泳' => '于會泳',
+'于根伟' => '于根偉',
+'于根偉' => '于根偉',
+'于格' => '于格',
+'于樂' => '于樂',
+'于树洁' => '于樹潔',
+'于樹潔' => '于樹潔',
+'于欣源' => '于欣源',
+'于正升' => '于正昇',
+'于正昇' => '于正昇',
+'于正昌' => '于正昌',
+'于归' => '于歸',
+'于永波' => '于永波',
+'于江震' => '于江震',
+'于波' => '于波',
+'于洪区' => '于洪區',
+'于洪區' => '于洪區',
+'于浩威' => '于浩威',
+'于海洋' => '于海洋',
+'于湘兰' => '于湘蘭',
+'于湘蘭' => '于湘蘭',
+'于漢超' => '于漢超',
+'于汉超' => '于漢超',
+'于泽尔' => '于澤爾',
+'于澤爾' => '于澤爾',
+'于涛' => '于濤',
+'于濤' => '于濤',
+'于爾岑' => '于爾岑',
+'于尔岑' => '于爾岑',
+'于尔根' => '于爾根',
+'于爾根' => '于爾根',
+'于尔里克' => '于爾里克',
+'于爾里克' => '于爾里克',
+'于特森' => '于特森',
+'于玉立' => '于玉立',
+'于田' => '于田',
+'于禁' => '于禁',
+'于秀敏' => '于秀敏',
+'于素秋' => '于素秋',
'于美人' => '于美人',
+'于若木' => '于若木',
+'于蔭霖' => '于蔭霖',
+'于荫霖' => '于蔭霖',
+'于衡' => '于衡',
+'于西翰' => '于西翰',
+'于謙' => '于謙',
+'于谦' => '于謙',
+'于貝爾' => '于貝爾',
+'于贝尔' => '于貝爾',
+'于赠' => '于贈',
+'于贈' => '于贈',
+'于越' => '于越',
+'于军' => '于軍',
+'于軍' => '于軍',
+'于道泉' => '于道泉',
+'于远伟' => '于遠偉',
+'于遠偉' => '于遠偉',
+'于都縣' => '于都縣',
+'于都县' => '于都縣',
+'于里察' => '于里察',
+'于阗' => '于闐',
+'于雙戈' => '于雙戈',
+'于双戈' => '于雙戈',
+'于震寰' => '于震寰',
+'于震环' => '于震環',
+'于震環' => '于震環',
+'于靖' => '于靖',
+'于非闇' => '于非闇',
+'于韋斯屈萊' => '于韋斯屈萊',
+'于韦斯屈莱' => '于韋斯屈萊',
+'于风政' => '于風政',
+'于風政' => '于風政',
+'于飞' => '于飛',
+'于余曲折' => '于餘曲折',
+'于凤桐' => '于鳳桐',
+'于鳳桐' => '于鳳桐',
+'于鳳至' => '于鳳至',
+'于凤至' => '于鳳至',
+'于默奥' => '于默奧',
+'于默奧' => '于默奧',
'云乎' => '云乎',
'云云' => '云云',
'云何' => '云何',
'云为' => '云為',
+'云為' => '云為',
'云然' => '云然',
'云尔' => '云爾',
-'互于' => '互於',
+'云:' => '云:',
+'五个' => '五個',
+'五出刊' => '五出刊',
+'五出口' => '五出口',
+'五出版' => '五出版',
+'五出生' => '五出生',
+'五出祁山' => '五出祁山',
+'五出逃' => '五出逃',
'五划' => '五劃',
'五天后' => '五天後',
'五岳' => '五嶽',
@@ -3091,16 +3383,16 @@ $zh2Hant = array(
'五谷王北街' => '五谷王北街',
'五谷王南街' => '五谷王南街',
'五只' => '五隻',
+'五余' => '五餘',
'五出' => '五齣',
'井干摧败' => '井榦摧敗',
'井里' => '井裡',
'亚于' => '亞於',
-'交于' => '交於',
+'亚美尼亚历' => '亞美尼亞曆',
'交托' => '交託',
'交游' => '交遊',
'交哄' => '交鬨',
'亦云' => '亦云',
-'亦于' => '亦於',
'亦庄亦谐' => '亦莊亦諧',
'亮丑' => '亮醜',
'亮钟' => '亮鐘',
@@ -3124,7 +3416,6 @@ $zh2Hant = array(
'人参选' => '人參選',
'人参酌' => '人參酌',
'人参阅' => '人參閱',
-'人口分布' => '人口分布',
'人后' => '人後',
'人欲' => '人慾',
'人物志' => '人物誌',
@@ -3133,18 +3424,25 @@ $zh2Hant = array(
'什么' => '什麼',
'仇仇' => '仇讎',
'今后' => '今後',
-'介于' => '介於',
+'他克制' => '他剋制',
+'他钟' => '他鐘',
'付托' => '付託',
'仙后座' => '仙后座',
'仙药' => '仙藥',
+'代码表' => '代碼表',
'令人发指' => '令人髮指',
'以后' => '以後',
'以自制' => '以自制',
'仰药' => '仰藥',
+'件钟' => '件鐘',
+'任何表' => '任何錶',
+'任何钟' => '任何鐘',
+'任何钟表' => '任何鐘錶',
'任教于' => '任教於',
'任于' => '任於',
'仿制' => '仿製',
'企划' => '企劃',
+'伊于湖底' => '伊于湖底',
'伊府面' => '伊府麵',
'伊斯兰教历' => '伊斯蘭教曆',
'伊斯兰教历史' => '伊斯蘭教歷史',
@@ -3160,9 +3458,17 @@ $zh2Hant = array(
'但云' => '但云',
'布于' => '佈於',
'布道' => '佈道',
+'布雷、' => '佈雷、',
+'布雷。' => '佈雷。',
+'布雷封锁' => '佈雷封鎖',
+'布雷的' => '佈雷的',
+'布雷艇' => '佈雷艇',
+'布雷舰' => '佈雷艦',
+'布雷速度' => '佈雷速度',
+'布雷,' => '佈雷,',
+'布雷;' => '佈雷;',
'位于' => '位於',
'位准' => '位準',
-'低于' => '低於',
'低洼' => '低洼',
'住扎' => '住紮',
'占0' => '佔0',
@@ -3336,6 +3642,7 @@ $zh2Hant = array(
'占过' => '佔過',
'占道' => '佔道',
'占零' => '佔零',
+'占領' => '佔領',
'占领' => '佔領',
'占头' => '佔頭',
'占头筹' => '佔頭籌',
@@ -3408,9 +3715,12 @@ $zh2Hant = array(
'余光中' => '余光中',
'余光生' => '余光生',
'佛罗棱萨' => '佛羅稜薩',
+'佛钟' => '佛鐘',
+'作品里' => '作品裡',
'作奸犯科' => '作姦犯科',
'作准' => '作準',
'作庄' => '作莊',
+'你克制' => '你剋制',
'你斗了胆' => '你斗了膽',
'你才子发昏' => '你纔子發昏',
'佣金收益' => '佣金收益',
@@ -3427,7 +3737,8 @@ $zh2Hant = array(
'并案' => '併案',
'并流' => '併流',
'并火' => '併火',
-'并为' => '併為',
+'并为一家' => '併為一家',
+'并为一体' => '併為一體',
'并产' => '併產',
'并当' => '併當',
'并叠' => '併疊',
@@ -3439,6 +3750,7 @@ $zh2Hant = array(
'并购' => '併購',
'并除' => '併除',
'并骨' => '併骨',
+'使其斗' => '使其鬥',
'来于' => '來於',
'来复' => '來複',
'侍仆' => '侍僕',
@@ -3449,7 +3761,6 @@ $zh2Hant = array(
'侵并' => '侵併',
'侵占到' => '侵占到',
'侵占罪' => '侵占罪',
-'便于' => '便於',
'便药' => '便藥',
'系数' => '係數',
'系为' => '係為',
@@ -3458,10 +3769,15 @@ $zh2Hant = array(
'信托贸易' => '信托貿易',
'信托' => '信託',
'修改后' => '修改後',
+'修杰楷' => '修杰楷',
'修炼' => '修鍊',
'修胡刀' => '修鬍刀',
'俯冲' => '俯衝',
+'个人' => '個人',
'个里' => '個裡',
+'个钟' => '個鐘',
+'个钟表' => '個鐘錶',
+'们克制' => '們剋制',
'们斗了胆' => '們斗了膽',
'倒绷孩儿' => '倒繃孩兒',
'幸免' => '倖免',
@@ -3475,7 +3791,6 @@ $zh2Hant = array(
'假发' => '假髮',
'偎干' => '偎乾',
'偏后' => '偏後',
-'偏于' => '偏於',
'做庄' => '做莊',
'停停当当' => '停停當當',
'停征' => '停徵',
@@ -3500,7 +3815,6 @@ $zh2Hant = array(
'传于' => '傳於',
'伤痕累累' => '傷痕纍纍',
'傻里傻气' => '傻裡傻氣',
-'倾向于' => '傾向於',
'倾复' => '傾複',
'仆人' => '僕人',
'仆使' => '僕使',
@@ -3530,9 +3844,11 @@ $zh2Hant = array(
'雇农' => '僱農',
'仪范' => '儀範',
'仪表' => '儀錶',
+'亿个' => '億個',
'亿多只' => '億多隻',
'亿天后' => '億天後',
'亿只' => '億隻',
+'亿余' => '億餘',
'俭仆' => '儉僕',
'俭朴' => '儉樸',
'俭确之教' => '儉确之教',
@@ -3555,6 +3871,8 @@ $zh2Hant = array(
'兀术' => '兀朮',
'元凶' => '元兇',
'充饥' => '充饑',
+'兆个' => '兆個',
+'兆余' => '兆餘',
'凶刀' => '兇刀',
'凶器' => '兇器',
'凶嫌' => '兇嫌',
@@ -3581,7 +3899,6 @@ $zh2Hant = array(
'先忧后乐' => '先憂後樂',
'先采' => '先採',
'先攻后守' => '先攻後守',
-'先于' => '先於',
'先盛后衰' => '先盛後衰',
'先礼后兵' => '先禮後兵',
'先义后利' => '先義後利',
@@ -3591,37 +3908,46 @@ $zh2Hant = array(
'先进后出' => '先進後出',
'先开花后结果' => '先開花後結果',
'光前裕后' => '光前裕後',
-'光采' => '光採',
'光致致' => '光緻緻',
'克药' => '克藥',
'克复' => '克複',
-'免于' => '免於',
+'免征' => '免徵',
'党参' => '党參',
'党太尉' => '党太尉',
'党怀英' => '党懷英',
'党进' => '党進',
'党项' => '党項',
'入夜后' => '入夜後',
-'入伙' => '入夥',
-'内心里' => '內心裡',
'内制' => '內製',
'内面包' => '內面包',
'内面包的' => '內面包的',
'内斗' => '內鬥',
'内哄' => '內鬨',
'全干' => '全乾',
+'全面包围' => '全面包圍',
+'全面包裹' => '全面包裹',
+'两个' => '兩個',
'两天后' => '兩天後',
'两天晒网' => '兩天晒網',
'两扎' => '兩紮',
'两虎共斗' => '兩虎共鬥',
'两只' => '兩隻',
+'两余' => '兩餘',
'两鼠斗穴' => '兩鼠鬥穴',
+'八个' => '八個',
+'八出刊' => '八出刊',
+'八出口' => '八出口',
+'八出版' => '八出版',
+'八出生' => '八出生',
+'八出祁山' => '八出祁山',
+'八出逃' => '八出逃',
'八大胡同' => '八大胡同',
'八天后' => '八天後',
'八字胡' => '八字鬍',
'八扎' => '八紮',
'八蜡' => '八蜡',
'八只' => '八隻',
+'八余' => '八餘',
'公仔面' => '公仔麵',
'公仆' => '公僕',
'公元后' => '公元後',
@@ -3631,13 +3957,23 @@ $zh2Hant = array(
'公历史' => '公歷史',
'公厘' => '公釐',
'公余' => '公餘',
+'六个' => '六個',
+'六出刊' => '六出刊',
+'六出口' => '六出口',
+'六出版' => '六出版',
+'六出生' => '六出生',
+'六出祁山' => '六出祁山',
+'六出逃' => '六出逃',
'六划' => '六劃',
'六天后' => '六天後',
'六谷' => '六穀',
'六扎' => '六紮',
'六冲' => '六衝',
'六只' => '六隻',
+'六余' => '六餘',
'六出' => '六齣',
+'共和历' => '共和曆',
+'共和历史' => '共和歷史',
'其一只' => '其一只',
'其二只' => '其二只',
'其八九只' => '其八九只',
@@ -3647,14 +3983,14 @@ $zh2Hant = array(
'典范' => '典範',
'兼并' => '兼并',
'冉有仆' => '冉有僕',
-'再于' => '再於',
'冗余' => '冗餘',
'冤仇' => '冤讎',
'冥蒙' => '冥濛',
+'冬天里' => '冬天裡',
'冬山庄' => '冬山庄',
+'冬日里' => '冬日裡',
'冬游' => '冬遊',
'冶游' => '冶遊',
-'冶炼' => '冶鍊',
'冷庄子' => '冷莊子',
'冷面相' => '冷面相',
'冷面' => '冷麵',
@@ -3682,7 +4018,6 @@ $zh2Hant = array(
'几筵' => '几筵',
'几丝' => '几絲',
'几面上' => '几面上',
-'凡于' => '凡於',
'凶杀案' => '凶殺案',
'凶相毕露' => '凶相畢露',
'凹洞里' => '凹洞裡',
@@ -3694,12 +4029,14 @@ $zh2Hant = array(
'出游' => '出遊',
'出丑' => '出醜',
'出锤' => '出鎚',
-'分布' => '分佈',
-'分布于' => '分佈於',
'分占' => '分佔',
-'分布区' => '分布區',
-'分布图' => '分布圖',
+'分别致' => '分别致',
+'分半钟' => '分半鐘',
+'分多钟' => '分多鐘',
+'分子钟' => '分子鐘',
'分布圖' => '分布圖',
+'分布图' => '分布圖',
+'分布于' => '分布於',
'分散于' => '分散於',
'分钟' => '分鐘',
'刑余' => '刑餘',
@@ -3712,8 +4049,8 @@ $zh2Hant = array(
'划着' => '划著',
'划着走' => '划著走',
'划龙舟' => '划龍舟',
+'判断发' => '判斷發',
'别后' => '別後',
-'别于' => '別於',
'别日南鸿才北去' => '別日南鴻纔北去',
'别致' => '別緻',
'别庄' => '別莊',
@@ -3728,19 +4065,23 @@ $zh2Hant = array(
'刮着' => '刮著',
'刮起来' => '刮起來',
'刮风下雪倒便宜' => '刮風下雪倒便宜',
-'刮胡刀' => '刮鬍刀',
+'刮胡' => '刮鬍',
'制冷机' => '制冷機',
'制签' => '制籤',
+'制钟' => '制鐘',
'刺绣' => '刺繡',
'刻划' => '刻劃',
-'刻于' => '刻於',
+'刻半钟' => '刻半鐘',
+'刻多钟' => '刻多鐘',
'刻钟' => '刻鐘',
'剃发' => '剃髮',
+'剃胡' => '剃鬍',
'剃须' => '剃鬚',
'削发' => '削髮',
'削面' => '削麵',
+'克制不了' => '剋制不了',
+'克制不住' => '剋制不住',
'克扣' => '剋扣',
-'克日' => '剋日',
'克星' => '剋星',
'克期' => '剋期',
'克死' => '剋死',
@@ -3762,6 +4103,7 @@ $zh2Hant = array(
'刚才一载' => '剛纔一載',
'剥制' => '剝製',
'剩余' => '剩餘',
+'剪其发' => '剪其髮',
'剪牡丹喂牛' => '剪牡丹喂牛',
'剪彩' => '剪綵',
'剪发' => '剪髮',
@@ -3794,6 +4136,7 @@ $zh2Hant = array(
'划归' => '劃歸',
'划法' => '劃法',
'划清' => '劃清',
+'划为' => '劃為',
'划界' => '劃界',
'划破' => '劃破',
'划线' => '劃線',
@@ -3802,11 +4145,12 @@ $zh2Hant = array(
'划开' => '劃開',
'剧药' => '劇藥',
'刘克庄' => '劉克莊',
+'力克制' => '力剋制',
'力拼' => '力拚',
'力拼众敌' => '力拼眾敵',
+'力求克制' => '力求剋制',
'力争上游' => '力爭上遊',
'功致' => '功緻',
-'加于' => '加於',
'加氢精制' => '加氫精制',
'加药' => '加藥',
'加注' => '加註',
@@ -3815,7 +4159,6 @@ $zh2Hant = array(
'劫后余生' => '劫後餘生',
'劫余' => '劫餘',
'勃郁' => '勃鬱',
-'勇于' => '勇於',
'动荡' => '動蕩',
'胜于' => '勝於',
'劳力士表' => '勞力士錶',
@@ -3826,6 +4169,7 @@ $zh2Hant = array(
'勾干' => '勾幹',
'勾心斗角' => '勾心鬥角',
'勾魂荡魄' => '勾魂蕩魄',
+'包括' => '包括',
'包准' => '包準',
'包谷' => '包穀',
'包扎' => '包紮',
@@ -3838,12 +4182,21 @@ $zh2Hant = array(
'匪干' => '匪幹',
'匿于' => '匿於',
'区划' => '區劃',
+'十个' => '十個',
+'十出刊' => '十出刊',
+'十出口' => '十出口',
+'十出版' => '十出版',
+'十出生' => '十出生',
+'十出祁山' => '十出祁山',
+'十出逃' => '十出逃',
'十划' => '十劃',
'十多只' => '十多隻',
'十天后' => '十天後',
'十扎' => '十紮',
'十只' => '十隻',
+'十余' => '十餘',
'十出' => '十齣',
+'千个' => '千個',
'千只可' => '千只可',
'千只够' => '千只夠',
'千只怕' => '千只怕',
@@ -3857,6 +4210,7 @@ $zh2Hant = array(
'千回百转' => '千迴百轉',
'千钧一发' => '千鈞一髮',
'千只' => '千隻',
+'千余' => '千餘',
'升官发财' => '升官發財',
'午后' => '午後',
'半制品' => '半制品',
@@ -3864,7 +4218,10 @@ $zh2Hant = array(
'半只够' => '半只夠',
'半于' => '半於',
'半只' => '半隻',
+'南京钟' => '南京鐘',
+'南京钟表' => '南京鐘錶',
'南宫适' => '南宮适',
+'南屏晚钟' => '南屏晚鐘',
'南岳' => '南嶽',
'南筑' => '南筑',
'南回线' => '南迴線',
@@ -3888,32 +4245,25 @@ $zh2Hant = array(
'厂部' => '厂部',
'厝薪于火' => '厝薪於火',
'原子钟' => '原子鐘',
-'原于' => '原於',
+'原钟' => '原鐘',
'历物之意' => '厤物之意',
'厥后' => '厥後',
-'参与' => '參与',
-'参与者' => '參与者',
'参合' => '參合',
'参考价值' => '參考價值',
+'参与' => '參與',
'参与人员' => '參與人員',
'参与制' => '參與制',
'参与感' => '參與感',
+'参与者' => '參與者',
'参观团' => '參觀團',
'参观团体' => '參觀團體',
'参阅' => '參閱',
-'及于' => '及於',
-'反于' => '反於',
'反朴' => '反樸',
'反冲' => '反衝',
'反复制' => '反複製',
'反复' => '反覆',
'反覆' => '反覆',
-'取信于' => '取信於',
'取舍' => '取捨',
-'取材于' => '取材於',
-'取决于' => '取決於',
-'取法于' => '取法於',
-'受制于' => '受制於',
'受托' => '受託',
'口干' => '口乾',
'口干冒' => '口干冒',
@@ -3924,12 +4274,16 @@ $zh2Hant = array(
'口燥唇干' => '口燥唇乾',
'口腹之欲' => '口腹之慾',
'口里' => '口裡',
+'口钟' => '口鐘',
'古书云' => '古書云',
+'古書云' => '古書云',
'古柯咸' => '古柯鹹',
'古朴' => '古樸',
'古语云' => '古語云',
+'古語云' => '古語云',
'古迹' => '古迹',
-'另于' => '另於',
+'古钟' => '古鐘',
+'古钟表' => '古鐘錶',
'另辟' => '另闢',
'叩钟' => '叩鐘',
'只占' => '只佔',
@@ -3964,16 +4318,24 @@ $zh2Hant = array(
'只采声' => '只采聲',
'叮叮当当' => '叮叮噹噹',
'叮当' => '叮噹',
-'可于' => '可於',
+'可以克制' => '可以剋制',
'可紧可松' => '可緊可鬆',
'可自制' => '可自制',
'台子女' => '台子女',
'台子孙' => '台子孫',
'台布景' => '台布景',
'台后' => '台後',
-'台历' => '台曆',
'台历史' => '台歷史',
+'台钟' => '台鐘',
'台面前' => '台面前',
+'叱咤903' => '叱咤903',
+'叱咤MY903' => '叱咤MY903',
+'叱咤My903' => '叱咤My903',
+'叱咤叱叱咤' => '叱咤叱叱咤',
+'叱咤叱咤叱咤咤' => '叱咤叱咤叱咤咤',
+'叱咤咤' => '叱咤咤',
+'叱咤乐坛' => '叱咤樂壇',
+'叱咤樂壇' => '叱咤樂壇',
'右后' => '右後',
'叶 恭弘' => '叶 恭弘',
'叶 恭弘' => '叶 恭弘',
@@ -3990,12 +4352,12 @@ $zh2Hant = array(
'吃辣面' => '吃辣麵',
'吃错药' => '吃錯藥',
'各辟' => '各闢',
+'各类钟' => '各類鐘',
'合伙人' => '合伙人',
'合并' => '合併',
'合伙' => '合夥',
'合府上' => '合府上',
'合采' => '合採',
-'合于' => '合於',
'合历' => '合曆',
'合历史' => '合歷史',
'合准' => '合準',
@@ -4011,6 +4373,9 @@ $zh2Hant = array(
'吊钟' => '吊鐘',
'同伙' => '同夥',
'同于' => '同於',
+'同余' => '同餘',
+'后丰' => '后豐',
+'后豐' => '后豐',
'后发座' => '后髮座',
'吐哺捉发' => '吐哺捉髮',
'吐哺握发' => '吐哺握髮',
@@ -4020,17 +4385,25 @@ $zh2Hant = array(
'向往时' => '向往時',
'向后' => '向後',
'向着' => '向著',
-'吝于' => '吝於',
'吞并' => '吞併',
'吟游' => '吟遊',
'含齿戴发' => '含齒戴髮',
'吹干' => '吹乾',
'吹发' => '吹髮',
+'吹胡' => '吹鬍',
+'吾为之范我驰驱' => '吾爲之範我馳驅',
'呆呆傻傻' => '呆呆傻傻',
'呆呆挣挣' => '呆呆掙掙',
+'呆呆兽' => '呆呆獸',
'呆呆笨笨' => '呆呆笨笨',
'呆致致' => '呆緻緻',
'呆里呆气' => '呆裡呆氣',
+'周一' => '周一',
+'周三' => '周三',
+'周二' => '周二',
+'周五' => '周五',
+'周六' => '周六',
+'周四' => '周四',
'周历' => '周曆',
'周杰伦' => '周杰倫',
'周杰倫' => '周杰倫',
@@ -4039,8 +4412,10 @@ $zh2Hant = array(
'周游' => '周遊',
'呼吁' => '呼籲',
'命中注定' => '命中注定',
+'和克制' => '和剋制',
'和奸' => '和姦',
'咎征' => '咎徵',
+'咕咕钟' => '咕咕鐘',
'咬姜呷醋' => '咬薑呷醋',
'咯当' => '咯噹',
'咳嗽药' => '咳嗽藥',
@@ -4066,18 +4441,24 @@ $zh2Hant = array(
'善后' => '善後',
'善于' => '善於',
'喜向往' => '喜向往',
+'喜欢表' => '喜歡錶',
+'喜欢钟' => '喜歡鐘',
+'喜欢钟表' => '喜歡鐘錶',
'喝干' => '喝乾',
'喧哄' => '喧鬨',
'丧钟' => '喪鐘',
'乔岳' => '喬嶽',
+'单于' => '單于',
+'单单于' => '單單於',
'单干' => '單幹',
'单打独斗' => '單打獨鬥',
'单只' => '單隻',
'嗑药' => '嗑藥',
'嗣后' => '嗣後',
+'嘀嗒的表' => '嘀嗒的錶',
'嘉谷' => '嘉穀',
'嘉肴' => '嘉肴',
-'嘴里' => '嘴裏',
+'嘴里' => '嘴裡',
'恶心' => '噁心',
'噙齿戴发' => '噙齒戴髮',
'喷洒' => '噴洒',
@@ -4094,7 +4475,14 @@ $zh2Hant = array(
'囉囉苏苏' => '囉囉囌囌',
'囉苏' => '囉囌',
'嘱托' => '囑託',
+'四个' => '四個',
+'四出刊' => '四出刊',
+'四出口' => '四出口',
'四出征收' => '四出徵收',
+'四出版' => '四出版',
+'四出生' => '四出生',
+'四出祁山' => '四出祁山',
+'四出逃' => '四出逃',
'四分历' => '四分曆',
'四分历史' => '四分歷史',
'四天后' => '四天後',
@@ -4102,6 +4490,8 @@ $zh2Hant = array(
'四扎' => '四紮',
'四只' => '四隻',
'四面包' => '四面包',
+'四面钟' => '四面鐘',
+'四余' => '四餘',
'四出' => '四齣',
'回采' => '回採',
'回旋加速' => '回旋加速',
@@ -4114,23 +4504,22 @@ $zh2Hant = array(
'回阳荡气' => '回陽蕩氣',
'因于' => '因於',
'困倦起来' => '困倦起來',
-'困于' => '困於',
'困兽之斗' => '困獸之鬥',
'困兽犹斗' => '困獸猶鬥',
'困斗' => '困鬥',
'固征' => '固徵',
-'固于' => '固於',
'囿于' => '囿於',
'圈占' => '圈佔',
'圈子里' => '圈子裡',
'圈梁' => '圈樑',
'圈里' => '圈裡',
'国之桢干' => '國之楨榦',
-'国家旅游局' => '國家旅游局',
'国于' => '國於',
'国历' => '國曆',
'国历代' => '國歷代',
+'国历任' => '國歷任',
'国历史' => '國歷史',
+'国历届' => '國歷屆',
'国仇' => '國讎',
'园里' => '園裡',
'园游会' => '園遊會',
@@ -4140,13 +4529,16 @@ $zh2Hant = array(
'土制' => '土製',
'土霉素' => '土霉素',
'在制品' => '在制品',
+'在克制' => '在剋制',
'在后' => '在後',
'在于' => '在於',
'地占' => '地佔',
+'地克制' => '地剋制',
'地方志' => '地方志',
'地志' => '地誌',
'地丑德齐' => '地醜德齊',
'坏于' => '坏於',
+'坐如钟' => '坐如鐘',
'坐庄' => '坐莊',
'坐钟' => '坐鐘',
'坑里' => '坑裡',
@@ -4155,16 +4547,21 @@ $zh2Hant = array(
'坦荡荡' => '坦蕩蕩',
'坱郁' => '坱鬱',
'垂于' => '垂於',
+'垂范' => '垂範',
'垂发' => '垂髮',
'型范' => '型範',
'埃及历' => '埃及曆',
'埃及历史' => '埃及歷史',
'埃荣冲' => '埃榮衝',
+'埋头寻表' => '埋頭尋錶',
+'埋头寻钟' => '埋頭尋鐘',
+'埋头寻钟表' => '埋頭尋鐘錶',
'城里' => '城裡',
'基干' => '基幹',
'基于' => '基於',
'基准' => '基準',
'坚致' => '堅緻',
+'堙淀' => '堙澱',
'涂着' => '塗著',
'涂药' => '塗藥',
'塞耳盗钟' => '塞耳盜鐘',
@@ -4187,6 +4584,7 @@ $zh2Hant = array(
'壸范' => '壼範',
'寿面' => '壽麵',
'夏天里' => '夏天裡',
+'夏日里' => '夏日裡',
'夏历' => '夏曆',
'夏历史' => '夏歷史',
'夏游' => '夏遊',
@@ -4195,7 +4593,12 @@ $zh2Hant = array(
'多占' => '多佔',
'多划' => '多劃',
'多半只' => '多半只',
+'多只可' => '多只可',
+'多只在' => '多只在',
'多只是' => '多只是',
+'多只会' => '多只會',
+'多只有' => '多只有',
+'多只能' => '多只能',
'多只需' => '多只需',
'多天后' => '多天後',
'多于' => '多於',
@@ -4207,6 +4610,7 @@ $zh2Hant = array(
'夜光表' => '夜光錶',
'夜里' => '夜裡',
'夜游' => '夜遊',
+'够克制' => '夠剋制',
'梦有五不占' => '夢有五不占',
'梦里' => '夢裡',
'梦游' => '夢遊',
@@ -4217,19 +4621,32 @@ $zh2Hant = array(
'伙计' => '夥計',
'大丑' => '大丑',
'大伙儿' => '大伙兒',
+'大只可' => '大只可',
+'大只在' => '大只在',
+'大只是' => '大只是',
+'大只会' => '大只會',
+'大只有' => '大只有',
+'大只能' => '大只能',
+'大只需' => '大只需',
+'大型钟' => '大型鐘',
+'大型钟表面' => '大型鐘表面',
+'大型钟表' => '大型鐘錶',
+'大型钟面' => '大型鐘面',
'大伙' => '大夥',
'大干' => '大幹',
'大批涌到' => '大批湧到',
'大折儿' => '大摺兒',
-'大于' => '大於',
'大明历' => '大明曆',
'大明历史' => '大明歷史',
'大历' => '大曆',
-'大梁' => '大樑',
+'大本钟' => '大本鐘',
+'大本钟敲' => '大本鐘敲',
'大历史' => '大歷史',
'大呆' => '大獃',
+'大病初愈' => '大病初癒',
'大目干连' => '大目乾連',
-'大胆' => '大胆',
+'大笨钟' => '大笨鐘',
+'大笨钟敲' => '大笨鐘敲',
'大蜡' => '大蜡',
'大衍历' => '大衍曆',
'大衍历史' => '大衍歷史',
@@ -4238,14 +4655,18 @@ $zh2Hant = array(
'大周折' => '大週摺',
'大金发苔' => '大金髮苔',
'大锤' => '大鎚',
+'大钟' => '大鐘',
'大只' => '大隻',
'大曲' => '大麴',
'天干物燥' => '天乾物燥',
'天克地冲' => '天克地衝',
'天后宫' => '天后宮',
'天后庙道' => '天后廟道',
+'天地志狼' => '天地志狼',
+'天地为范' => '天地為範',
'天干地支' => '天干地支',
'天后' => '天後',
+'天文学钟' => '天文學鐘',
'天文钟' => '天文鐘',
'天翻地覆' => '天翻地覆',
'天覆地载' => '天覆地載',
@@ -4253,7 +4674,6 @@ $zh2Hant = array(
'太初历' => '太初曆',
'太初历史' => '太初歷史',
'夯干' => '夯幹',
-'失于' => '失於',
'夸人' => '夸人',
'夸克' => '夸克',
'夸夸其谈' => '夸夸其談',
@@ -4269,7 +4689,6 @@ $zh2Hant = array(
'奇迹' => '奇迹',
'奇丑' => '奇醜',
'奏折' => '奏摺',
-'奏于' => '奏於',
'奥占' => '奧佔',
'夺斗' => '奪鬥',
'奋斗' => '奮鬥',
@@ -4279,6 +4698,7 @@ $zh2Hant = array(
'女仆' => '女僕',
'奴仆' => '奴僕',
'奸淫掳掠' => '奸淫擄掠',
+'她克制' => '她剋制',
'好干' => '好乾',
'好家伙' => '好傢夥',
'好勇斗狠' => '好勇鬥狠',
@@ -4294,13 +4714,13 @@ $zh2Hant = array(
'好签' => '好籤',
'好丑' => '好醜',
'好斗' => '好鬥',
-'如于' => '如於',
'如果干' => '如果幹',
'如饥似渴' => '如饑似渴',
'妙药' => '妙藥',
'始于' => '始於',
'委托' => '委託',
'委托书' => '委託書',
+'姜文杰' => '姜文杰',
'奸夫' => '姦夫',
'奸妇' => '姦婦',
'奸宄' => '姦宄',
@@ -4315,23 +4735,23 @@ $zh2Hant = array(
'婚后' => '婚後',
'婢仆' => '婢僕',
'娲杆' => '媧杆',
-'嫁于' => '嫁於',
'嫁祸于' => '嫁禍於',
'嫌凶' => '嫌兇',
'嫌好道丑' => '嫌好道醜',
-'娴于' => '嫻於',
'嬉游' => '嬉遊',
'嬖幸' => '嬖倖',
'嬴余' => '嬴餘',
'子之丰兮' => '子之丰兮',
'子云' => '子云',
'字汇' => '字彙',
+'字码表' => '字碼表',
'字里行间' => '字裡行間',
'存十一于千百' => '存十一於千百',
'存折' => '存摺',
'存于' => '存於',
'季后赛' => '季後賽',
'孤寡不谷' => '孤寡不穀',
+'学里' => '學裡',
'宇宙志' => '宇宙誌',
'守先待后' => '守先待後',
'安于' => '安於',
@@ -4349,12 +4769,10 @@ $zh2Hant = array(
'定于' => '定於',
'定准' => '定準',
'定制' => '定製',
-'宜于' => '宜於',
+'宜云' => '宜云',
'宣泄' => '宣洩',
'宦游' => '宦遊',
'宫里' => '宮裡',
-'宰相肚里好撑船' => '宰相肚裡好撐船',
-'宰相肚里能撑船' => '宰相肚裡能撐船',
'害于' => '害於',
'宴游' => '宴遊',
'家仆' => '家僕',
@@ -4370,18 +4788,15 @@ $zh2Hant = array(
'容于' => '容於',
'容范' => '容範',
'寄托在' => '寄托在',
-'寄于' => '寄於',
'寄托' => '寄託',
'密致' => '密緻',
'寇准' => '寇準',
'寇仇' => '寇讎',
-'富于' => '富於',
'富余' => '富餘',
+'寒假里' => '寒假裡',
'寒栗' => '寒慄',
'寒于' => '寒於',
-'寓情于景' => '寓情於景',
'寓于' => '寓於',
-'寓禁于征' => '寓禁於徵',
'寡占' => '寡佔',
'寡欲' => '寡慾',
'实干' => '實幹',
@@ -4392,21 +4807,26 @@ $zh2Hant = array(
'宽松' => '寬鬆',
'寮采' => '寮寀',
'宝山庄' => '寶山庄',
-'宝历' => '寶曆',
'寶曆' => '寶曆',
+'宝历' => '寶曆',
'宝历史' => '寶歷史',
'宝庄' => '寶莊',
'宝里宝气' => '寶裡寶氣',
+'寸发千金' => '寸髮千金',
+'寺钟' => '寺鐘',
'封面里' => '封面裡',
'射雕' => '射鵰',
'将占' => '將佔',
'将占卜' => '將占卜',
-'将于' => '將於',
'专向往' => '專向往',
'专注' => '專註',
+'专辑里' => '專輯裡',
'对折' => '對摺',
'对于' => '對於',
'对准' => '對準',
+'对准表' => '對準錶',
+'对准钟' => '對準鐘',
+'对准钟表' => '對準鐘錶',
'对华发动' => '對華發動',
'对表中' => '對表中',
'对表扬' => '對表揚',
@@ -4420,17 +4840,25 @@ $zh2Hant = array(
'小价' => '小价',
'小仆' => '小僕',
'小几' => '小几',
+'小只可' => '小只可',
+'小只在' => '小只在',
+'小只是' => '小只是',
+'小只会' => '小只會',
+'小只有' => '小只有',
+'小只能' => '小只能',
+'小只需' => '小只需',
+'小型钟' => '小型鐘',
+'小型钟表面' => '小型鐘表面',
+'小型钟表' => '小型鐘錶',
+'小型钟面' => '小型鐘面',
'小伙子' => '小夥子',
-'小于' => '小於',
'小米面' => '小米麵',
'小只' => '小隻',
'少占' => '少佔',
'少采' => '少採',
-'少于' => '少於',
-'就于' => '就於',
+'就克制' => '就剋制',
'就范' => '就範',
'就里' => '就裡',
-'就读于' => '就讀於',
'尸位素餐' => '尸位素餐',
'尸利' => '尸利',
'尸居余气' => '尸居餘氣',
@@ -4440,16 +4868,13 @@ $zh2Hant = array(
'尸谏' => '尸諫',
'尸魂界' => '尸魂界',
'尸鸠' => '尸鳩',
-'尼克松' => '尼克鬆',
'局里' => '局裡',
'屁股大吊了心' => '屁股大弔了心',
-'居于' => '居於',
'屋子里' => '屋子裡',
'屋梁' => '屋樑',
'屋里' => '屋裡',
'屑于' => '屑於',
'屡顾尔仆' => '屢顧爾僕',
-'属意于' => '屬意於',
'属于' => '屬於',
'属托' => '屬託',
'屯扎' => '屯紮',
@@ -4458,7 +4883,9 @@ $zh2Hant = array(
'山岳' => '山嶽',
'山后' => '山後',
'山梁' => '山樑',
+'山洞里' => '山洞裡',
'山棱' => '山稜',
+'山羊胡' => '山羊鬍',
'山庄' => '山莊',
'山药' => '山藥',
'山里' => '山裡',
@@ -4483,7 +4910,6 @@ $zh2Hant = array(
'巡回医疗' => '巡回醫療',
'巡回' => '巡迴',
'巡游' => '巡遊',
-'工于' => '工於',
'工致' => '工緻',
'左后' => '左後',
'左冲右突' => '左衝右突',
@@ -4498,13 +4924,13 @@ $zh2Hant = array(
'已占' => '已佔',
'已占卜' => '已占卜',
'已占算' => '已占算',
-'已于' => '已於',
'巴尔干' => '巴爾幹',
'巷里' => '巷裡',
'市占' => '市佔',
'市占率' => '市佔率',
'市里' => '市裡',
'布谷' => '布穀',
+'布谷鸟钟' => '布穀鳥鐘',
'布庄' => '布莊',
'布谷鸟' => '布谷鳥',
'希伯来历' => '希伯來曆',
@@ -4523,6 +4949,7 @@ $zh2Hant = array(
'平平当当' => '平平當當',
'平泉庄' => '平泉莊',
'平准' => '平準',
+'年代里' => '年代裡',
'年后' => '年後',
'年历' => '年曆',
'年历史' => '年歷史',
@@ -4536,6 +4963,7 @@ $zh2Hant = array(
'并迭' => '并迭',
'幸免于难' => '幸免於難',
'幸于' => '幸於',
+'幸运胡' => '幸運鬍',
'干上' => '幹上',
'干下去' => '幹下去',
'干不了' => '幹不了',
@@ -4593,7 +5021,6 @@ $zh2Hant = array(
'干么' => '幹麼',
'几划' => '幾劃',
'几天后' => '幾天後',
-'几于' => '幾於',
'几只' => '幾隻',
'几出' => '幾齣',
'广部' => '广部',
@@ -4681,8 +5108,7 @@ $zh2Hant = array(
'張三丰' => '張三丰',
'张勋' => '張勳',
'强占' => '強佔',
-'强加于' => '強加于',
-'强加于人' => '強加於人',
+'强制作用' => '強制作用',
'强奸' => '強姦',
'强干' => '強幹',
'强于' => '強於',
@@ -4707,9 +5133,9 @@ $zh2Hant = array(
'形于' => '形於',
'仿佛' => '彷彿',
'役于' => '役於',
+'彼此克制' => '彼此剋制',
'往后' => '往後',
'往日無仇' => '往日無讎',
-'往肚里吞' => '往肚裡吞',
'往里' => '往裡',
'往复' => '往複',
'很干' => '很乾',
@@ -4892,14 +5318,11 @@ $zh2Hant = array(
'后龙' => '後龍',
'徐干' => '徐幹',
'徒托空言' => '徒託空言',
-'得于' => '得於',
-'徜徉于' => '徜徉於',
-'从事于' => '從事於',
+'得克制' => '得剋制',
'从于' => '從於',
'从里到外' => '從裡到外',
'从里向外' => '從裡向外',
'复始' => '復始',
-'复仇' => '復讎',
'征人' => '徵人',
'征令' => '徵令',
'征占' => '徵佔',
@@ -4953,25 +5376,136 @@ $zh2Hant = array(
'德占' => '德佔',
'心愿' => '心愿',
'心于' => '心於',
+'心理' => '心理',
'心细如发' => '心細如髮',
+'心系一' => '心繫一',
+'心系世' => '心繫世',
+'心系中' => '心繫中',
+'心系乔' => '心繫乔',
+'心系五' => '心繫五',
+'心系京' => '心繫京',
+'心系人' => '心繫人',
+'心系他' => '心繫他',
+'心系伊' => '心繫伊',
+'心系何' => '心繫何',
+'心系你' => '心繫你',
+'心系健' => '心繫健',
+'心系传' => '心繫傳',
+'心系全' => '心繫全',
+'心系两' => '心繫兩',
+'心系农' => '心繫农',
+'心系功' => '心繫功',
+'心系动' => '心繫動',
+'心系募' => '心繫募',
+'心系北' => '心繫北',
+'心系十' => '心繫十',
+'心系千' => '心繫千',
+'心系南' => '心繫南',
+'心系台' => '心繫台',
+'心系和' => '心繫和',
+'心系哪' => '心繫哪',
+'心系唐' => '心繫唐',
+'心系嘱' => '心繫囑',
+'心系四' => '心繫四',
+'心系困' => '心繫困',
+'心系国' => '心繫國',
+'心系在' => '心繫在',
+'心系地' => '心繫地',
+'心系大' => '心繫大',
+'心系天' => '心繫天',
+'心系夫' => '心繫夫',
+'心系奥' => '心繫奧',
+'心系女' => '心繫女',
+'心系她' => '心繫她',
+'心系妻' => '心繫妻',
+'心系妇' => '心繫婦',
+'心系子' => '心繫子',
+'心系它' => '心繫它',
+'心系宣' => '心繫宣',
+'心系家' => '心繫家',
+'心系富' => '心繫富',
+'心系小' => '心繫小',
+'心系山' => '心繫山',
+'心系川' => '心繫川',
+'心系幼' => '心繫幼',
+'心系广' => '心繫廣',
+'心系彼' => '心繫彼',
+'心系德' => '心繫德',
+'心系您' => '心繫您',
+'心系慈' => '心繫慈',
+'心系我' => '心繫我',
+'心系摩' => '心繫摩',
+'心系故' => '心繫故',
+'心系新' => '心繫新',
+'心系日' => '心繫日',
+'心系昌' => '心繫昌',
+'心系晓' => '心繫曉',
+'心系曼' => '心繫曼',
+'心系东' => '心繫東',
+'心系林' => '心繫林',
+'心系母' => '心繫母',
+'心系民' => '心繫民',
+'心系江' => '心繫江',
+'心系汶' => '心繫汶',
+'心系沈' => '心繫沈',
+'心系沙' => '心繫沙',
+'心系泰' => '心繫泰',
+'心系浙' => '心繫浙',
+'心系港' => '心繫港',
+'心系湖' => '心繫湖',
+'心系澳' => '心繫澳',
+'心系灾' => '心繫災',
+'心系父' => '心繫父',
+'心系生' => '心繫生',
+'心系病' => '心繫病',
+'心系百' => '心繫百',
+'心系的' => '心繫的',
+'心系众' => '心繫眾',
+'心系社' => '心繫社',
+'心系祖' => '心繫祖',
+'心系神' => '心繫神',
+'心系红' => '心繫紅',
+'心系美' => '心繫美',
+'心系群' => '心繫群',
+'心系老' => '心繫老',
+'心系舞' => '心繫舞',
+'心系英' => '心繫英',
+'心系茶' => '心繫茶',
+'心系万' => '心繫萬',
+'心系着' => '心繫著',
+'心系兰' => '心繫蘭',
+'心系西' => '心繫西',
+'心系贫' => '心繫貧',
+'心系输' => '心繫輸',
+'心系近' => '心繫近',
+'心系远' => '心繫遠',
+'心系选' => '心繫選',
+'心系重' => '心繫重',
+'心系长' => '心繫長',
+'心系阮' => '心繫阮',
+'心系震' => '心繫震',
+'心系非' => '心繫非',
+'心系风' => '心繫風',
+'心系香' => '心繫香',
+'心系高' => '心繫高',
+'心系麦' => '心繫麥',
+'心系黄' => '心繫黃',
+'心脏' => '心臟',
'心荡' => '心蕩',
'心药' => '心藥',
-'心里' => '心裏',
-'心里不安' => '心裡不安',
-'心里有数' => '心裡有數',
-'心里话' => '心裡話',
-'心里头' => '心裡頭',
+'心里面' => '心裏面',
+'心里' => '心裡',
'心长发短' => '心長髮短',
'心余' => '心餘',
-'志于' => '志於',
+'必须' => '必須',
'忙并' => '忙併',
-'忙于' => '忙於',
'忙里' => '忙裡',
'忙里偷闲' => '忙裡偷閒',
'忠人之托' => '忠人之托',
'忠仆' => '忠僕',
'忠于' => '忠於',
'快干' => '快乾',
+'快克制' => '快剋制',
'快快当当' => '快快當當',
'快冲' => '快衝',
'忽前忽后' => '忽前忽後',
@@ -4989,7 +5523,6 @@ $zh2Hant = array(
'性欲' => '性慾',
'怪里怪气' => '怪裡怪氣',
'怫郁' => '怫鬱',
-'怯于' => '怯於',
'恂栗' => '恂慄',
'恒生指数' => '恒生指數',
'恒生股价指数' => '恒生股價指數',
@@ -5003,30 +5536,29 @@ $zh2Hant = array(
'悠悠荡荡' => '悠悠蕩蕩',
'悠荡' => '悠蕩',
'悠游' => '悠遊',
+'您克制' => '您剋制',
'悲筑' => '悲筑',
'悲郁' => '悲鬱',
-'闷在心里' => '悶在心裡',
'闷着头儿干' => '悶著頭兒幹',
'悸栗' => '悸慄',
'情欲' => '情慾',
'惇朴' => '惇樸',
'恶直丑正' => '惡直醜正',
'恶斗' => '惡鬥',
+'想克制' => '想剋制',
'惴栗' => '惴慄',
'意占' => '意佔',
+'意克制' => '意剋制',
'意大利面' => '意大利麵',
'意面' => '意麵',
-'爱在心里' => '愛在心裡',
'爱困' => '愛睏',
'感冒药' => '感冒藥',
'感于' => '感於',
-'愧于' => '愧於',
'愿朴' => '愿樸',
'愿而恭' => '愿而恭',
'栗冽' => '慄冽',
'栗栗' => '慄慄',
'慌里慌张' => '慌裡慌張',
-'惯于' => '慣於',
'庆吊' => '慶弔',
'庆历' => '慶曆',
'庆历史' => '慶歷史',
@@ -5046,6 +5578,7 @@ $zh2Hant = array(
'凭借着' => '憑藉著',
'恳托' => '懇託',
'懈松' => '懈鬆',
+'应克制' => '應剋制',
'应征' => '應徵',
'应钟' => '應鐘',
'懔栗' => '懍慄',
@@ -5054,19 +5587,18 @@ $zh2Hant = array(
'蒙直' => '懞直',
'惩前毖后' => '懲前毖後',
'惩忿窒欲' => '懲忿窒欲',
-'懒于' => '懶於',
'怀里' => '懷裡',
'怀表' => '懷錶',
-'悬挂' => '懸挂',
+'怀钟' => '懷鐘',
'悬梁' => '懸樑',
'悬臂梁' => '懸臂樑',
'悬钟' => '懸鐘',
-'惧于' => '懼於',
'懿范' => '懿範',
'恋恋不舍' => '戀戀不捨',
'成于' => '成於',
+'成于思' => '成於思',
'成药' => '成藥',
-'或于' => '或於',
+'我克制' => '我剋制',
'戬谷' => '戩穀',
'截发' => '截髮',
'战天斗地' => '戰天鬥地',
@@ -5074,6 +5606,7 @@ $zh2Hant = array(
'战栗' => '戰慄',
'战斗' => '戰鬥',
'戏彩娱亲' => '戲綵娛親',
+'戏里' => '戲裡',
'戴表' => '戴錶',
'戴发含齿' => '戴髮含齒',
'房里' => '房裡',
@@ -5100,6 +5633,7 @@ $zh2Hant = array(
'手里' => '手裡',
'手表' => '手錶',
'手松' => '手鬆',
+'才克制' => '才剋制',
'才干休' => '才干休',
'才干戈' => '才干戈',
'才干扰' => '才干擾',
@@ -5119,12 +5653,14 @@ $zh2Hant = array(
'打吨' => '打吨',
'打干' => '打幹',
'打拼' => '打拚',
+'打断发' => '打斷發',
'打谷' => '打穀',
+'打着钟' => '打著鐘',
'打路庄板' => '打路莊板',
'打钟' => '打鐘',
'打斗' => '打鬥',
-'托福考' => '托福考',
'托管国' => '托管國',
+'扛大梁' => '扛大樑',
'扞御' => '扞禦',
'扯面' => '扯麵',
'扶余国' => '扶餘國',
@@ -5158,8 +5694,10 @@ $zh2Hant = array(
'抽公签' => '抽公籤',
'抽签' => '抽籤',
'抿发' => '抿髮',
+'拂钟无声' => '拂鐘無聲',
'拆伙' => '拆夥',
'拈须' => '拈鬚',
+'拉克施尔德钟' => '拉克施爾德鐘',
'拉杆' => '拉杆',
'拉纤' => '拉縴',
'拉面上' => '拉面上',
@@ -5175,13 +5713,19 @@ $zh2Hant = array(
'拒人于' => '拒人於',
'拒于' => '拒於',
'拓朴' => '拓樸',
+'拔发' => '拔髮',
+'拔须' => '拔鬚',
'拗别' => '拗彆',
'拘于' => '拘於',
'拙于' => '拙於',
'拙朴' => '拙樸',
+'拼却' => '拚卻',
'拼命' => '拚命',
'拼舍' => '拚捨',
'拼死' => '拚死',
+'拼生尽死' => '拚生盡死',
+'拼绝' => '拚絕',
+'拼老命' => '拚老命',
'拼斗' => '拚鬥',
'拜托' => '拜託',
'括发' => '括髮',
@@ -5189,6 +5733,8 @@ $zh2Hant = array(
'拮据' => '拮据',
'拼死拼活' => '拼死拼活',
'拾沈' => '拾瀋',
+'拿下表' => '拿下錶',
+'拿下钟' => '拿下鐘',
'拿准' => '拿準',
'拿破仑' => '拿破崙',
'挂名' => '挂名',
@@ -5198,10 +5744,10 @@ $zh2Hant = array(
'挂念' => '挂念',
'挂号' => '挂號',
'挂车' => '挂車',
-'挂钩' => '挂鉤',
'挂面' => '挂面',
'指手划脚' => '指手劃腳',
'挌斗' => '挌鬥',
+'挑大梁' => '挑大樑',
'挑斗' => '挑鬥',
'振荡' => '振蕩',
'捆扎' => '捆紮',
@@ -5272,6 +5818,8 @@ $zh2Hant = array(
'掌柜' => '掌柜',
'排骨面' => '排骨麵',
'挂帘' => '掛帘',
+'挂历' => '掛曆',
+'挂钩' => '掛鈎',
'挂钟' => '掛鐘',
'采下' => '採下',
'采伐' => '採伐',
@@ -5317,6 +5865,7 @@ $zh2Hant = array(
'采种' => '採種',
'采空区' => '採空區',
'采空采穗' => '採空採穗',
+'采納' => '採納',
'采纳' => '採納',
'采给' => '採給',
'采花' => '採花',
@@ -5347,6 +5896,7 @@ $zh2Hant = array(
'采盐' => '採鹽',
'掣签' => '掣籤',
'接着说' => '接著說',
+'控制' => '控制',
'推情准理' => '推情準理',
'推托之词' => '推托之詞',
'推舟于陆' => '推舟於陸',
@@ -5362,6 +5912,8 @@ $zh2Hant = array(
'握发' => '握髮',
'揩干' => '揩乾',
'揪采' => '揪採',
+'揪发' => '揪髮',
+'揪须' => '揪鬚',
'揭丑' => '揭醜',
'挥手表' => '揮手表',
'挥杆' => '揮杆',
@@ -5380,6 +5932,7 @@ $zh2Hant = array(
'摧坚获丑' => '摧堅獲醜',
'摭采' => '摭採',
'摸棱' => '摸稜',
+'摸钟' => '摸鐘',
'折合' => '摺合',
'折奏' => '摺奏',
'折子' => '摺子',
@@ -5396,7 +5949,6 @@ $zh2Hant = array(
'捞干' => '撈乾',
'捞面' => '撈麵',
'撚须' => '撚鬚',
-'撞木钟' => '撞木鐘',
'撞球台' => '撞球檯',
'撞钟' => '撞鐘',
'撞阵冲军' => '撞陣衝軍',
@@ -5408,9 +5960,9 @@ $zh2Hant = array(
'扑冬' => '撲鼕',
'扑冬冬' => '撲鼕鼕',
'擀面' => '擀麵',
-'擅于' => '擅於',
'击扑' => '擊扑',
'击钟' => '擊鐘',
+'操作钟' => '操作鐘',
'担仔面' => '擔仔麵',
'担担面' => '擔擔麵',
'担着' => '擔著',
@@ -5422,33 +5974,29 @@ $zh2Hant = array(
'擦干' => '擦乾',
'擦干净' => '擦乾淨',
'擦药' => '擦藥',
-'拟于' => '擬於',
'拧干' => '擰乾',
'摆钟' => '擺鐘',
-'摄于' => '攝於',
'摄制' => '攝製',
'支干' => '支幹',
'支杆' => '支杆',
'收获' => '收穫',
'改征' => '改徵',
-'改于' => '改於',
'攻占' => '攻佔',
-'放在心里' => '放在心裡',
'放蒙挣' => '放懞掙',
'放荡' => '放蕩',
'放松' => '放鬆',
-'故于' => '故於',
+'故事里' => '故事裡',
+'故云' => '故云',
'敏于' => '敏於',
-'敏于事而慎于言' => '敏於事而慎於言',
'救药' => '救藥',
'败于' => '敗於',
'叙说着' => '敘說著',
+'教学钟' => '教學鐘',
'教于' => '教於',
'教范' => '教範',
'敢干' => '敢幹',
'敢情欲' => '敢情欲',
'敢斗了胆' => '敢斗了膽',
-'敢于' => '敢於',
'散伙' => '散夥',
'散于' => '散於',
'散荡' => '散蕩',
@@ -5458,10 +6006,14 @@ $zh2Hant = array(
'敲钟' => '敲鐘',
'整庄' => '整莊',
'整只' => '整隻',
+'整发用品' => '整髮用品',
'敌后' => '敵後',
'敌忾同仇' => '敵愾同讎',
'敷药' => '敷藥',
'数天后' => '數天後',
+'数字表' => '數字錶',
+'数字钟' => '數字鐘',
+'数字钟表' => '數字鐘錶',
'数罪并罚' => '數罪併罰',
'数与虏确' => '數與虜确',
'文丑' => '文丑',
@@ -5515,6 +6067,7 @@ $zh2Hant = array(
'于你' => '於你',
'于八' => '於八',
'于六' => '於六',
+'于克制' => '於剋制',
'于前' => '於前',
'于劣' => '於劣',
'于勤' => '於勤',
@@ -5535,24 +6088,20 @@ $zh2Hant = array(
'于它' => '於它',
'于家' => '於家',
'于密' => '於密',
-'于左' => '於左',
'于差' => '於差',
'于己' => '於己',
'于市' => '於市',
'于幕' => '於幕',
-'于幼华' => '於幼華',
'于弱' => '於弱',
'于强' => '於強',
-'于征' => '於征',
'于后' => '於後',
+'于征' => '於徵',
'于心' => '於心',
-'于思' => '於思',
'于怀' => '於懷',
'于我' => '於我',
'于戏' => '於戲',
'于敝' => '於敝',
'于斯' => '於斯',
-'于于' => '於於',
'于是' => '於是',
'于是乎' => '於是乎',
'于时' => '於時',
@@ -5569,7 +6118,6 @@ $zh2Hant = array(
'于焉' => '於焉',
'于墙' => '於牆',
'于物' => '於物',
-'于田' => '於田',
'于毕' => '於畢',
'于尽' => '於盡',
'于盲' => '於盲',
@@ -5590,7 +6138,6 @@ $zh2Hant = array(
'于丑' => '於醜',
'于野' => '於野',
'于陆' => '於陸',
-'于飞' => '於飛',
'于0' => '於0',
'于1' => '於1',
'于2' => '於2',
@@ -5607,7 +6154,6 @@ $zh2Hant = array(
'施药' => '施藥',
'旁征博引' => '旁徵博引',
'旁注' => '旁註',
-'旅游业' => '旅游業',
'旅游' => '旅遊',
'旋干转坤' => '旋乾轉坤',
'旋绕着' => '旋繞著',
@@ -5615,6 +6161,7 @@ $zh2Hant = array(
'族里' => '族裡',
'旗杆' => '旗杆',
'日占' => '日佔',
+'日子里' => '日子裡',
'日后' => '日後',
'日晒' => '日晒',
'日历' => '日曆',
@@ -5627,23 +6174,25 @@ $zh2Hant = array(
'升阳' => '昇陽',
'昊天不吊' => '昊天不弔',
'明征' => '明徵',
-'明于' => '明於',
'明目张胆' => '明目張胆',
'明窗净几' => '明窗淨几',
'明范' => '明範',
'明里' => '明裡',
+'易克制' => '易剋制',
'易于' => '易於',
+'星巴克' => '星巴克',
'星历' => '星曆',
'星期后' => '星期後',
'星历史' => '星歷史',
'星辰表' => '星辰錶',
'春假里' => '春假裡',
'春天里' => '春天裡',
+'春日里' => '春日裡',
'春药' => '春藥',
'春游' => '春遊',
'春香斗学' => '春香鬥學',
-'昧于' => '昧於',
'时钟' => '時鐘',
+'时间里' => '時間裡',
'晃荡' => '晃蕩',
'晋升' => '晉陞',
'晒干' => '晒乾',
@@ -5656,6 +6205,8 @@ $zh2Hant = array(
'晒种' => '晒種',
'晒衣' => '晒衣',
'晒黑' => '晒黑',
+'晚于' => '晚於',
+'晚钟' => '晚鐘',
'晞发' => '晞髮',
'晨钟' => '晨鐘',
'普冬冬' => '普鼕鼕',
@@ -5663,12 +6214,12 @@ $zh2Hant = array(
'晾干' => '晾乾',
'晕船药' => '暈船藥',
'晕车药' => '暈車藥',
+'暑假里' => '暑假裡',
'暗地里' => '暗地裡',
'暗沟里' => '暗溝裡',
'暗里' => '暗裡',
'暗斗' => '暗鬥',
'畅游' => '暢遊',
-'暂于' => '暫於',
'暴敛横征' => '暴斂橫徵',
'暴晒' => '暴晒',
'历元' => '曆元',
@@ -5688,43 +6239,49 @@ $zh2Hant = array(
'曰云' => '曰云',
'更仆难数' => '更僕難數',
'更签' => '更籤',
+'更钟' => '更鐘',
'书后' => '書後',
'书呆子' => '書獃子',
'书签' => '書籤',
'曼谷人' => '曼谷人',
-'曾于' => '曾於',
'曾朴' => '曾樸',
-'最多只' => '最多只',
+'最多' => '最多',
'最后' => '最後',
-'最里面' => '最裡面',
+'会上签署' => '會上簽署',
+'会上签订' => '會上簽訂',
'会占' => '會佔',
'会占卜' => '會占卜',
'会干' => '會幹',
'会吊' => '會弔',
'会后' => '會後',
-'会于' => '會於',
'会里' => '會裡',
'月后' => '月後',
'月历' => '月曆',
'月历史' => '月歷史',
'月离于毕' => '月離於畢',
+'月面' => '月面',
'月丽于箕' => '月麗於箕',
+'有事之无范' => '有事之無範',
'有仆' => '有僕',
'有够赞' => '有夠讚',
+'有征伐' => '有征伐',
+'有征战' => '有征戰',
+'有征服' => '有征服',
+'有征讨' => '有征討',
'有征' => '有徵',
'有恒街' => '有恒街',
'有栖川' => '有栖川',
'有准' => '有準',
'有棱有角' => '有稜有角',
-'有鉴于' => '有鑑於',
-'有鉴于此' => '有鑒於此',
'有只' => '有隻',
'有余' => '有餘',
'有发头陀寺' => '有髮頭陀寺',
-'服务于' => '服務於',
'服于' => '服於',
'服药' => '服藥',
'望了望' => '望了望',
+'望着表' => '望著錶',
+'望着钟' => '望著鐘',
+'望着钟表' => '望著鐘錶',
'朝乾夕惕' => '朝乾夕惕',
'朝后' => '朝後',
'朝钟' => '朝鐘',
@@ -5735,6 +6292,7 @@ $zh2Hant = array(
'木材干馏' => '木材乾餾',
'木梁' => '木樑',
'木制' => '木製',
+'木钟' => '木鐘',
'未干' => '未乾',
'末药' => '末藥',
'本征' => '本徵',
@@ -5747,6 +6305,7 @@ $zh2Hant = array(
'李連杰' => '李連杰',
'李连杰' => '李連杰',
'材干' => '材幹',
+'村子里' => '村子裡',
'村庄' => '村莊',
'村落发' => '村落發',
'村里' => '村裡',
@@ -5755,7 +6314,9 @@ $zh2Hant = array(
'束发' => '束髮',
'杯干' => '杯乾',
'杯面' => '杯麵',
+'杰伦' => '杰倫',
'杰特' => '杰特',
+'东周钟' => '東周鐘',
'东岳' => '東嶽',
'东冲西突' => '東衝西突',
'东游' => '東遊',
@@ -5768,10 +6329,11 @@ $zh2Hant = array(
'林钟' => '林鐘',
'果干' => '果乾',
'果子干' => '果子乾',
-'果于' => '果於',
'枝不得大于干' => '枝不得大於榦',
'枝干' => '枝幹',
'枯干' => '枯乾',
+'台历' => '枱曆',
+'架钟' => '架鐘',
'某只' => '某隻',
'染指于' => '染指於',
'染发' => '染髮',
@@ -5787,6 +6349,7 @@ $zh2Hant = array(
'格于' => '格於',
'格范' => '格範',
'格里历' => '格里曆',
+'格里高利历' => '格里高利曆',
'格斗' => '格鬥',
'桂圆干' => '桂圓乾',
'桅杆' => '桅杆',
@@ -5801,31 +6364,28 @@ $zh2Hant = array(
'梯冲' => '梯衝',
'械系' => '械繫',
'械斗' => '械鬥',
-'弃妻女于不顾' => '棄妻女於不顧',
'弃舍' => '棄捨',
'棉制' => '棉製',
'棒子面' => '棒子麵',
'枣庄' => '棗莊',
'栋梁' => '棟樑',
'棫朴' => '棫樸',
-'栖于' => '棲於',
+'森林里' => '森林裡',
+'棺材里' => '棺材裡',
'植发' => '植髮',
'椰枣干' => '椰棗乾',
'楚庄问鼎' => '楚莊問鼎',
'楚庄王' => '楚莊王',
'楚庄绝缨' => '楚莊絕纓',
'桢干' => '楨幹',
-'业精于勤荒于嬉' => '業精於勤荒於嬉',
'业余' => '業餘',
'榨干' => '榨乾',
'荣登后座' => '榮登后座',
'杠杆' => '槓桿',
-'乐意于' => '樂意於',
-'乐于' => '樂於',
+'乐器钟' => '樂器鐘',
'樊于期' => '樊於期',
'梁上' => '樑上',
'梁柱' => '樑柱',
-'标志着' => '標志著',
'标杆' => '標杆',
'标标致致' => '標標致致',
'标准' => '標準',
@@ -5861,7 +6421,11 @@ $zh2Hant = array(
'树干' => '樹榦',
'树梁' => '樹樑',
'桥梁' => '橋樑',
+'機械系' => '機械系',
'机械系' => '機械系',
+'机械表' => '機械錶',
+'机械钟' => '機械鐘',
+'机械钟表' => '機械鐘錶',
'机绣' => '機繡',
'横征暴敛' => '橫徵暴斂',
'横杆' => '橫杆',
@@ -5875,7 +6439,6 @@ $zh2Hant = array(
'柜台' => '櫃檯',
'栉发工' => '櫛髮工',
'栏杆' => '欄杆',
-'次于' => '次於',
'欲海难填' => '欲海難填',
'欺蒙' => '欺矇',
'歇后' => '歇後',
@@ -5885,20 +6448,17 @@ $zh2Hant = array(
'止于' => '止於',
'止痛药' => '止痛藥',
'止血药' => '止血藥',
+'正在叱咤' => '正在叱咤',
'正官庄' => '正官庄',
'正后' => '正後',
-'正于' => '正於',
'正当着' => '正當著',
'此后' => '此後',
-'步步高升' => '步步高升',
'武丑' => '武丑',
'武斗' => '武鬥',
'岁聿云暮' => '歲聿云暮',
+'历史里' => '歷史裡',
'归并' => '歸併',
-'归功于' => '歸功於',
-'归咎于' => '歸咎於',
'归于' => '歸於',
-'归罪于' => '歸罪於',
'归余' => '歸餘',
'歹斗' => '歹鬥',
'死后' => '死後',
@@ -5920,7 +6480,6 @@ $zh2Hant = array(
'殴斗' => '毆鬥',
'母范' => '母範',
'母丑' => '母醜',
-'每于' => '每於',
'每每只' => '每每只',
'每只' => '每隻',
'毒药' => '毒藥',
@@ -5930,7 +6489,6 @@ $zh2Hant = array(
'毛发' => '毛髮',
'毫厘' => '毫釐',
'毫发' => '毫髮',
-'气在心里' => '氣在心裡',
'气冲斗牛' => '氣沖斗牛',
'气郁' => '氣鬱',
'氤郁' => '氤鬱',
@@ -5952,7 +6510,6 @@ $zh2Hant = array(
'沈淀' => '沈澱',
'沈着' => '沈著',
'沈郁' => '沈鬱',
-'沉湎于' => '沉湎於',
'沉淀' => '沉澱',
'沉郁' => '沉鬱',
'没干没净' => '沒乾沒淨',
@@ -5970,6 +6527,7 @@ $zh2Hant = array(
'河里' => '河裡',
'油斗' => '油鬥',
'油面' => '油麵',
+'治愈' => '治癒',
'沿溯' => '沿泝',
'法占' => '法佔',
'法自制' => '法自制',
@@ -5980,6 +6538,7 @@ $zh2Hant = array(
'波发藻' => '波髮藻',
'泥于' => '泥於',
'注云' => '注云',
+'注释' => '注釋',
'泰山梁木' => '泰山梁木',
'泱郁' => '泱鬱',
'泳气钟' => '泳氣鐘',
@@ -6003,7 +6562,6 @@ $zh2Hant = array(
'洪适' => '洪适',
'洪钟' => '洪鐘',
'汹涌' => '洶湧',
-'活动于' => '活動於',
'派团参加' => '派團參加',
'流征' => '流徵',
'流于' => '流於',
@@ -6015,21 +6573,28 @@ $zh2Hant = array(
'浪琴表' => '浪琴錶',
'浪荡' => '浪蕩',
'浪游' => '浪遊',
-'浮夸' => '浮夸',
'浮于' => '浮於',
-'浮游动物' => '浮游動物',
-'浮游植物' => '浮游植物',
-'浮游生物' => '浮游生物',
'浮荡' => '浮蕩',
-'浮游' => '浮遊',
+'浮夸' => '浮誇',
'浮松' => '浮鬆',
+'海上布雷' => '海上佈雷',
'海干' => '海乾',
-'浸于' => '浸於',
+'海湾布雷' => '海灣佈雷',
+'涂坤' => '涂坤',
'涂壮勋' => '涂壯勳',
'涂壯勳' => '涂壯勳',
+'涂天相' => '涂天相',
+'涂序瑄' => '涂序瑄',
+'涂澤民' => '涂澤民',
+'涂泽民' => '涂澤民',
+'涂绍煃' => '涂紹煃',
+'涂羽卿' => '涂羽卿',
'涂謹申' => '涂謹申',
'涂谨申' => '涂謹申',
+'涂逢年' => '涂逢年',
'涂醒哲' => '涂醒哲',
+'涂長望' => '涂長望',
+'涂长望' => '涂長望',
'涂鸿钦' => '涂鴻欽',
'涂鴻欽' => '涂鴻欽',
'消炎药' => '消炎藥',
@@ -6050,15 +6615,17 @@ $zh2Hant = array(
'淫欲' => '淫慾',
'淫荡' => '淫蕩',
'淬炼' => '淬鍊',
-'深于' => '深於',
+'深山何处钟' => '深山何處鐘',
+'深渊里' => '深淵裡',
+'淳于' => '淳于',
'淳朴' => '淳樸',
'渊淳岳峙' => '淵淳嶽峙',
+'浅淀' => '淺澱',
'清心寡欲' => '清心寡欲',
'清汤挂面' => '清湯掛麵',
'减肥药' => '減肥藥',
'渠冲' => '渠衝',
'港制' => '港製',
-'游牧民族' => '游牧民族',
'浑朴' => '渾樸',
'浑个' => '渾箇',
'凑合着' => '湊合著',
@@ -6115,7 +6682,6 @@ $zh2Hant = array(
'溟蒙' => '溟濛',
'溢于' => '溢於',
'溲面' => '溲麵',
-'溶于' => '溶於',
'溺于' => '溺於',
'滃郁' => '滃鬱',
'滑借' => '滑藉',
@@ -6130,8 +6696,9 @@ $zh2Hant = array(
'卤制' => '滷製',
'卤鸡' => '滷雞',
'卤面' => '滷麵',
-'满于' => '滿於',
+'满拼自尽' => '滿拚自盡',
'满满当当' => '滿滿當當',
+'满头洋发' => '滿頭洋髮',
'漂荡' => '漂蕩',
'漕挽' => '漕輓',
'沤郁' => '漚鬱',
@@ -6139,19 +6706,26 @@ $zh2Hant = array(
'汉弥登钟表公司' => '漢彌登鐘錶公司',
'漫游' => '漫遊',
'潜意识里' => '潛意識裡',
+'潜水表' => '潛水錶',
'潜水钟' => '潛水鐘',
+'潜水钟表' => '潛水鐘錶',
'潭里' => '潭裡',
'潮涌' => '潮湧',
'溃于' => '潰於',
'澄澹精致' => '澄澹精致',
'澒蒙' => '澒濛',
'泽渗漓而下降' => '澤滲灕而下降',
+'淀乃不耕之地' => '澱乃不耕之地',
+'淀北片' => '澱北片',
+'淀山' => '澱山',
+'淀淀' => '澱澱',
+'淀积' => '澱積',
'淀粉' => '澱粉',
+'淀解物' => '澱解物',
+'淀谓之滓' => '澱謂之滓',
'澹台' => '澹臺',
'澹荡' => '澹蕩',
-'激于' => '激於',
'激荡' => '激蕩',
-'浓于' => '濃於',
'浓发' => '濃髮',
'蒙汜' => '濛汜',
'蒙蒙细雨' => '濛濛細雨',
@@ -6167,14 +6741,13 @@ $zh2Hant = array(
'沈海' => '瀋海',
'沈海铁路' => '瀋海鐵路',
'沈阳' => '瀋陽',
-'濒于' => '瀕於',
'潇洒' => '瀟洒',
'弥山遍野' => '瀰山遍野',
'弥漫' => '瀰漫',
'弥漫着' => '瀰漫著',
'弥弥' => '瀰瀰',
-'灌于' => '灌於',
'灌药' => '灌藥',
+'漓水' => '灕水',
'漓江' => '灕江',
'漓湘' => '灕湘',
'漓然' => '灕然',
@@ -6183,6 +6756,7 @@ $zh2Hant = array(
'火并' => '火併',
'火拼' => '火拚',
'火折子' => '火摺子',
+'火箭布雷' => '火箭佈雷',
'火签' => '火籤',
'火药' => '火藥',
'灰蒙' => '灰濛',
@@ -6206,6 +6780,7 @@ $zh2Hant = array(
'无征不信' => '無徵不信',
'无业游民' => '無業游民',
'无梁楼盖' => '無樑樓蓋',
+'无法克制' => '無法剋制',
'无药可救' => '無藥可救',
'無言不仇' => '無言不讎',
'无余' => '無餘',
@@ -6224,20 +6799,24 @@ $zh2Hant = array(
'煨干' => '煨乾',
'煮面' => '煮麵',
'荧郁' => '熒鬱',
-'熔于' => '熔於',
'熬药' => '熬藥',
'炖药' => '燉藥',
'燎发' => '燎髮',
'烧干' => '燒乾',
'燕几' => '燕几',
'燕巢于幕' => '燕巢於幕',
+'燕燕于飞' => '燕燕于飛',
'燕游' => '燕遊',
+'烫一个发' => '燙一個髮',
+'烫一次发' => '燙一次髮',
+'烫个发' => '燙個髮',
+'烫完发' => '燙完髮',
+'烫次发' => '燙次髮',
'烫发' => '燙髮',
'烫面' => '燙麵',
'营干' => '營幹',
'烬余' => '燼餘',
'争先恐后' => '爭先恐後',
-'争名于朝,争利于市' => '爭名於朝,爭利於市',
'争奇斗妍' => '爭奇鬥妍',
'争奇斗异' => '爭奇鬥異',
'争奇斗艳' => '爭奇鬥豔',
@@ -6256,13 +6835,13 @@ $zh2Hant = array(
'牛肉面' => '牛肉麵',
'牛只' => '牛隻',
'物欲' => '物慾',
+'特别致' => '特别致',
'特制住' => '特制住',
'特制定' => '特制定',
'特制止' => '特制止',
'特制订' => '特制訂',
'特征' => '特徵',
'特效药' => '特效藥',
-'特于' => '特於',
'特制' => '特製',
'牵一发' => '牽一髮',
'牵挂' => '牽挂',
@@ -6272,10 +6851,12 @@ $zh2Hant = array(
'狂并潮' => '狂併潮',
'狃于' => '狃於',
'狐借虎威' => '狐藉虎威',
-'狗嘴里' => '狗嘴裡',
'猛于' => '猛於',
'猛冲' => '猛衝',
'猜三划五' => '猜三划五',
+'犹如表' => '猶如錶',
+'犹如钟' => '猶如鐘',
+'犹如钟表' => '猶如鐘錶',
'呆串了皮' => '獃串了皮',
'呆事' => '獃事',
'呆人' => '獃人',
@@ -6294,6 +6875,7 @@ $zh2Hant = array(
'呆着' => '獃著',
'呆话' => '獃話',
'呆头' => '獃頭',
+'狱里' => '獄裡',
'奖杯' => '獎盃',
'独占' => '獨佔',
'独占鳌头' => '獨佔鰲頭',
@@ -6306,18 +6888,21 @@ $zh2Hant = array(
'玉历史' => '玉歷史',
'王庄' => '王莊',
'王余鱼' => '王餘魚',
-'玩弄于股掌之上' => '玩弄於股掌之上',
'珍肴异馔' => '珍肴異饌',
'班里' => '班裡',
'现于' => '現於',
'球杆' => '球杆',
+'理一个发' => '理一個髮',
+'理一次发' => '理一次髮',
+'理个发' => '理個髮',
+'理完发' => '理完髮',
+'理次发' => '理次髮',
'理发' => '理髮',
'琴钟' => '琴鐘',
'瑞征' => '瑞徵',
'瑶签' => '瑤籤',
'环游' => '環遊',
'瓮安' => '甕安',
-'甘于' => '甘於',
'甚于' => '甚於',
'甚么' => '甚麼',
'甜水面' => '甜水麵',
@@ -6325,14 +6910,13 @@ $zh2Hant = array(
'生力面' => '生力麵',
'生于' => '生於',
'生殖洄游' => '生殖洄游',
+'生物钟' => '生物鐘',
'生发生' => '生發生',
'生姜' => '生薑',
'生锈' => '生鏽',
'生发' => '生髮',
'产卵洄游' => '產卵洄游',
'产后' => '產後',
-'产于' => '產於',
-'用于' => '用於',
'用药' => '用藥',
'甩发' => '甩髮',
'田谷' => '田穀',
@@ -6351,15 +6935,10 @@ $zh2Hant = array(
'毕业于' => '畢業於',
'毕生发展' => '畢生發展',
'画着' => '畫著',
-'异于' => '異於',
-'当一天和尚撞一天钟' => '當一天和尚撞一天鐘',
-'当一天和尚,撞一天钟' => '當一天和尚,撞一天鐘',
'当家才知柴米价' => '當家纔知柴米價',
-'当于' => '當於',
'当准' => '當準',
'当当丁丁' => '當當丁丁',
'当着' => '當著',
-'疏于' => '疏於',
'疏松' => '疏鬆',
'疑系' => '疑係',
'疑凶' => '疑兇',
@@ -6368,26 +6947,27 @@ $zh2Hant = array(
'病后' => '病後',
'病后初愈' => '病後初癒',
'病征' => '病徵',
+'病愈' => '病癒',
'病余' => '病餘',
'症候群' => '症候群',
'痊愈' => '痊癒',
'痒疹' => '痒疹',
'痒痒' => '痒痒',
'痕迹' => '痕迹',
-'痴呆症' => '痴呆症',
-'痴呆' => '痴獃',
+'愈合' => '癒合',
'症候' => '癥候',
'症状' => '癥狀',
'症结' => '癥結',
'癸丑' => '癸丑',
'发干' => '發乾',
-'发于' => '發於',
'发汗药' => '發汗藥',
'发呆' => '發獃',
'发蒙' => '發矇',
'发签' => '發籤',
'发庄' => '發莊',
'发着' => '發著',
+'发表' => '發表',
+'發表' => '發表',
'发松' => '發鬆',
'发面' => '發麵',
'白干' => '白乾',
@@ -6401,7 +6981,9 @@ $zh2Hant = array(
'白粉面' => '白粉麵',
'白里透红' => '白裡透紅',
'白发' => '白髮',
+'白胡' => '白鬍',
'白霉' => '白黴',
+'百个' => '百個',
'百只可' => '百只可',
'百只够' => '百只夠',
'百只怕' => '百只怕',
@@ -6409,6 +6991,7 @@ $zh2Hant = array(
'百多只' => '百多隻',
'百天后' => '百天後',
'百拙千丑' => '百拙千醜',
+'百科里' => '百科裡',
'百谷' => '百穀',
'百扎' => '百紮',
'百花历' => '百花曆',
@@ -6416,6 +6999,11 @@ $zh2Hant = array(
'百药之长' => '百藥之長',
'百炼' => '百鍊',
'百只' => '百隻',
+'百余' => '百餘',
+'的克制' => '的剋制',
+'的钟' => '的鐘',
+'的钟表' => '的鐘錶',
+'皆可作淀' => '皆可作澱',
'皆准' => '皆準',
'皇天后土' => '皇天后土',
'皇历' => '皇曆',
@@ -6425,8 +7013,8 @@ $zh2Hant = array(
'皇庄' => '皇莊',
'皓发' => '皓髮',
'皮制服' => '皮制服',
-'皮里阳秋' => '皮裏陽秋',
'皮里春秋' => '皮裡春秋',
+'皮里阳秋' => '皮裡陽秋',
'皮制' => '皮製',
'皮松' => '皮鬆',
'皱别' => '皺彆',
@@ -6438,6 +7026,7 @@ $zh2Hant = array(
'盛赞' => '盛讚',
'盗采' => '盜採',
'盗钟' => '盜鐘',
+'尽量克制' => '盡量剋制',
'监制' => '監製',
'盘里' => '盤裡',
'盘回' => '盤迴',
@@ -6454,11 +7043,20 @@ $zh2Hant = array(
'相于' => '相於',
'相冲' => '相衝',
'相斗' => '相鬥',
+'看下表' => '看下錶',
+'看下钟' => '看下鐘',
'看准' => '看準',
+'看着表' => '看著錶',
+'看着钟' => '看著鐘',
+'看着钟表' => '看著鐘錶',
+'看表面' => '看表面',
+'看表' => '看錶',
+'看钟' => '看鐘',
'真凶' => '真兇',
'真个' => '真箇',
'眼帘' => '眼帘',
'眼眶里' => '眼眶裡',
+'眼睛里' => '眼睛裡',
'眼药' => '眼藥',
'眼里' => '眼裡',
'困乏' => '睏乏',
@@ -6467,7 +7065,12 @@ $zh2Hant = array(
'睡着了' => '睡著了',
'睡游病' => '睡遊病',
'瞄准' => '瞄準',
+'瞅下表' => '瞅下錶',
+'瞅下钟' => '瞅下鐘',
'瞠乎后矣' => '瞠乎後矣',
+'瞧着表' => '瞧著錶',
+'瞧着钟' => '瞧著鐘',
+'瞧着钟表' => '瞧著鐘錶',
'了望' => '瞭望',
'了然' => '瞭然',
'了若指掌' => '瞭若指掌',
@@ -6493,8 +7096,11 @@ $zh2Hant = array(
'石家庄' => '石家莊',
'石梁' => '石樑',
'石英表' => '石英錶',
+'石英钟' => '石英鐘',
+'石英钟表' => '石英鐘錶',
'石莼' => '石蓴',
'石钟乳' => '石鐘乳',
+'矽谷' => '矽谷',
'研制' => '研製',
'砰当' => '砰噹',
'朱唇皓齿' => '硃唇皓齒',
@@ -6508,11 +7114,12 @@ $zh2Hant = array(
'确瘠' => '确瘠',
'碑志' => '碑誌',
'碰钟' => '碰鐘',
+'码表' => '碼錶',
'磁制' => '磁製',
'磨制' => '磨製',
'磨炼' => '磨鍊',
+'磬钟' => '磬鐘',
'硗确' => '磽确',
-'碍于' => '礙於',
'碍难照准' => '礙難照准',
'砻谷机' => '礱穀機',
'示范' => '示範',
@@ -6543,8 +7150,10 @@ $zh2Hant = array(
'私下里' => '私下裡',
'私欲' => '私慾',
'私斗' => '私鬥',
+'秋假里' => '秋假裡',
'秋天里' => '秋天裡',
'秋后' => '秋後',
+'秋日里' => '秋日裡',
'秋裤' => '秋褲',
'秋游' => '秋遊',
'秋阴入井干' => '秋陰入井幹',
@@ -6552,6 +7161,7 @@ $zh2Hant = array(
'种师中' => '种師中',
'种师道' => '种師道',
'种放' => '种放',
+'科斗' => '科斗',
'科范' => '科範',
'秒表明' => '秒表明',
'秒表示' => '秒表示',
@@ -6561,7 +7171,6 @@ $zh2Hant = array(
'稀松' => '稀鬆',
'税后' => '稅後',
'税后净利' => '稅後淨利',
-'税捐稽征处' => '稅捐稽征處',
'稍后' => '稍後',
'棱台' => '稜台',
'棱子' => '稜子',
@@ -6604,22 +7213,25 @@ $zh2Hant = array(
'谷道' => '穀道',
'谷雨' => '穀雨',
'谷类' => '穀類',
-'谷风' => '穀風',
'谷食' => '穀食',
'穆罕默德历' => '穆罕默德曆',
'穆罕默德历史' => '穆罕默德歷史',
'积极参与' => '積极參与',
'积极参加' => '積极參加',
+'积淀' => '積澱',
'积谷' => '積穀',
'积谷防饥' => '積穀防饑',
'积郁' => '積鬱',
'稳占' => '穩佔',
'稳扎' => '穩紮',
+'空中布雷' => '空中佈雷',
+'空投布雷' => '空投佈雷',
'空蒙' => '空濛',
'空荡' => '空蕩',
'空荡荡' => '空蕩蕩',
'空谷回音' => '空谷回音',
'空钟' => '空鐘',
+'空余' => '空餘',
'窒欲' => '窒慾',
'窗台上' => '窗台上',
'窗帘' => '窗帘',
@@ -6634,7 +7246,6 @@ $zh2Hant = array(
'立于' => '立於',
'立范' => '立範',
'站干岸儿' => '站乾岸兒',
-'竟于' => '竟於',
'童仆' => '童僕',
'端庄' => '端莊',
'竞斗' => '競鬥',
@@ -6643,6 +7254,7 @@ $zh2Hant = array(
'竹签' => '竹籤',
'笑里藏刀' => '笑裡藏刀',
'笨笨呆呆' => '笨笨呆呆',
+'第四出局' => '第四出局',
'笔划' => '筆劃',
'笔秃墨干' => '筆禿墨乾',
'等于' => '等於',
@@ -6676,7 +7288,6 @@ $zh2Hant = array(
'个中资讯' => '箇中資訊',
'个中高手' => '箇中高手',
'个旧' => '箇舊',
-'算在里面' => '算在裡面',
'算历' => '算曆',
'算历史' => '算歷史',
'算准' => '算準',
@@ -6688,10 +7299,13 @@ $zh2Hant = array(
'节余' => '節餘',
'范例' => '範例',
'范围' => '範圍',
+'范字' => '範字',
'范式' => '範式',
+'范性形变' => '範性形變',
'范文' => '範文',
'范本' => '範本',
'范畴' => '範疇',
+'范金' => '範金',
'简并' => '簡併',
'简朴' => '簡樸',
'簸荡' => '簸蕩',
@@ -6723,6 +7337,7 @@ $zh2Hant = array(
'糕干' => '糕乾',
'粪秽蔑面' => '糞穢衊面',
'团子' => '糰子',
+'系列里' => '系列裡',
'系着' => '系著',
'系里' => '系裡',
'纪元后' => '紀元後',
@@ -6730,6 +7345,7 @@ $zh2Hant = array(
'纪历史' => '紀歷史',
'约占' => '約佔',
'红绳系足' => '紅繩繫足',
+'红钟' => '紅鐘',
'红霉素' => '紅霉素',
'红发' => '紅髮',
'纡回' => '紆迴',
@@ -6741,7 +7357,6 @@ $zh2Hant = array(
'素朴' => '素樸',
'素发' => '素髮',
'素面' => '素麵',
-'索我于枯鱼之肆' => '索我於枯魚之肆',
'索马里' => '索馬里',
'索馬里' => '索馬里',
'索面' => '索麵',
@@ -6763,6 +7378,7 @@ $zh2Hant = array(
'扎起' => '紮起',
'扎铁' => '紮鐵',
'细不容发' => '細不容髮',
+'细如发' => '細如髮',
'细致' => '細緻',
'细炼' => '細鍊',
'终于' => '終於',
@@ -6777,6 +7393,7 @@ $zh2Hant = array(
'绝后' => '絕後',
'绝于' => '絕於',
'绞干' => '絞乾',
+'络腮胡' => '絡腮鬍',
'给我干脆' => '給我干脆',
'给于' => '給於',
'丝来线去' => '絲來線去',
@@ -6792,6 +7409,7 @@ $zh2Hant = array(
'绑扎' => '綁紮',
'綑扎' => '綑紮',
'经有云' => '經有云',
+'經有云' => '經有云',
'绿发' => '綠髮',
'绸缎庄' => '綢緞莊',
'维系' => '維繫',
@@ -6818,6 +7436,7 @@ $zh2Hant = array(
'编余' => '編余',
'编制法' => '編制法',
'编采' => '編採',
+'编码表' => '編碼表',
'编制' => '編製',
'编钟' => '編鐘',
'编发' => '編髮',
@@ -6835,7 +7454,9 @@ $zh2Hant = array(
'纤夫' => '縴夫',
'纤手' => '縴手',
'总后' => '總後',
+'总裁制' => '總裁制',
'繁复' => '繁複',
+'繁钟' => '繁鐘',
'绷住' => '繃住',
'绷子' => '繃子',
'绷带' => '繃帶',
@@ -6871,7 +7492,7 @@ $zh2Hant = array(
'系怀' => '繫懷',
'系恋' => '繫戀',
'系于' => '繫於',
-'系系' => '繫系',
+'系于一发' => '繫於一髮',
'系结' => '繫結',
'系紧' => '繫緊',
'系绳' => '繫繩',
@@ -6892,6 +7513,7 @@ $zh2Hant = array(
'坛坛罐罐' => '罈罈罐罐',
'坛騞' => '罈騞',
'置于' => '置於',
+'置言成范' => '置言成範',
'骂着' => '罵著',
'罢于' => '罷於',
'羁系' => '羈繫',
@@ -6901,13 +7523,11 @@ $zh2Hant = array(
'美制' => '美製',
'美丑' => '美醜',
'美发' => '美髮',
-'羞于' => '羞於',
'群丑' => '群醜',
-'羨余' => '羨餘',
+'羡余' => '羨餘',
'义占' => '義佔',
'义仆' => '義僕',
'义庄' => '義莊',
-'习于' => '習於',
'翕辟' => '翕闢',
'翱游' => '翱遊',
'翻涌' => '翻湧',
@@ -6918,24 +7538,23 @@ $zh2Hant = array(
'老干部' => '老幹部',
'老蒙' => '老懞',
'老于' => '老於',
+'老爷钟' => '老爺鐘',
'老庄' => '老莊',
'老姜' => '老薑',
'老板' => '老闆',
'老面皮' => '老面皮',
'考后' => '考後',
'考征' => '考徵',
+'而克制' => '而剋制',
'而后' => '而後',
-'而于' => '而於',
'耍斗' => '耍鬥',
'耕佣' => '耕傭',
'耕获' => '耕穫',
'耳后' => '耳後',
'耳余' => '耳餘',
-'耽于' => '耽於',
'耿于' => '耿於',
'聊斋志异' => '聊齋志異',
'聘雇' => '聘僱',
-'联于' => '聯於',
'联系' => '聯繫',
'听于' => '聽於',
'肉干' => '肉乾',
@@ -6943,11 +7562,13 @@ $zh2Hant = array(
'肉丝面' => '肉絲麵',
'肉羹面' => '肉羹麵',
'肉松' => '肉鬆',
-'肚里' => '肚裏',
+'肚里' => '肚裡',
+'肝脏' => '肝臟',
'肝郁' => '肝鬱',
'股栗' => '股慄',
'肥筑方言' => '肥筑方言',
'肴馔' => '肴饌',
+'肺脏' => '肺臟',
'胃药' => '胃藥',
'胃里' => '胃裡',
'背向着' => '背向著',
@@ -6957,8 +7578,10 @@ $zh2Hant = array(
'胜肽' => '胜肽',
'胜键' => '胜鍵',
'胡云' => '胡云',
+'胡子昂' => '胡子昂',
'胡朴安' => '胡樸安',
'胡里胡涂' => '胡裡胡塗',
+'能克制' => '能剋制',
'能干休' => '能干休',
'能干戈' => '能干戈',
'能干扰' => '能干擾',
@@ -6973,12 +7596,15 @@ $zh2Hant = array(
'脊梁' => '脊樑',
'脱谷机' => '脫穀機',
'脱发' => '脫髮',
+'脾脏' => '脾臟',
'腊之以为饵' => '腊之以為餌',
'腊味' => '腊味',
'腊毒' => '腊毒',
'腊笔' => '腊筆',
+'肾脏' => '腎臟',
'腐干' => '腐乾',
'腐余' => '腐餘',
+'腕表' => '腕錶',
'脑子里' => '腦子裡',
'脑干' => '腦幹',
'脑后' => '腦後',
@@ -6986,6 +7612,7 @@ $zh2Hant = array(
'脚注' => '腳註',
'脚炼' => '腳鍊',
'膏药' => '膏藥',
+'肤发' => '膚髮',
'胶卷' => '膠捲',
'膨松' => '膨鬆',
'臣仆' => '臣僕',
@@ -7011,11 +7638,12 @@ $zh2Hant = array(
'自于' => '自於',
'自制' => '自製',
'自觉自愿' => '自覺自愿',
+'至多' => '至多',
'至于' => '至於',
-'致力于' => '致力於',
'致于' => '致於',
'臻于' => '臻於',
'舂谷' => '舂穀',
+'与克制' => '與剋制',
'兴致' => '興緻',
'举手表' => '舉手表',
'举手表决' => '舉手表決',
@@ -7024,6 +7652,9 @@ $zh2Hant = array(
'旧历史' => '舊歷史',
'旧药' => '舊藥',
'旧游' => '舊遊',
+'旧表' => '舊錶',
+'旧钟' => '舊鐘',
+'旧钟表' => '舊鐘錶',
'舌干唇焦' => '舌乾唇焦',
'舌后' => '舌後',
'舒卷' => '舒捲',
@@ -7045,19 +7676,28 @@ $zh2Hant = array(
'花盆里' => '花盆裡',
'花庵词选' => '花菴詞選',
'花药' => '花藥',
+'花钟' => '花鐘',
'花马吊嘴' => '花馬弔嘴',
'花哄' => '花鬨',
'苑里' => '苑裡',
'若干' => '若干',
-'若于' => '若於',
'苦干' => '苦幹',
-'苦于' => '苦於',
'苦药' => '苦藥',
-'苦里' => '苦裏',
+'苦里' => '苦裡',
'苦斗' => '苦鬥',
'苎麻' => '苧麻',
'英占' => '英佔',
'苹萦' => '苹縈',
+'茂都淀' => '茂都澱',
+'范文同' => '范文同',
+'范文正公' => '范文正公',
+'范文瀾' => '范文瀾',
+'范文澜' => '范文瀾',
+'范文照' => '范文照',
+'范文程' => '范文程',
+'范文芳' => '范文芳',
+'范文藤' => '范文藤',
+'范文虎' => '范文虎',
'范登堡' => '范登堡',
'茶几' => '茶几',
'茶庄' => '茶莊',
@@ -7100,20 +7740,26 @@ $zh2Hant = array(
'菜肴' => '菜肴',
'菠棱菜' => '菠稜菜',
'菠萝干' => '菠蘿乾',
+'华严钟' => '華嚴鐘',
'华发' => '華髮',
'万一只' => '萬一只',
+'万个' => '萬個',
'万多只' => '萬多隻',
'万天后' => '萬天後',
+'万年历表' => '萬年曆錶',
'万历' => '萬曆',
'万历史' => '萬歷史',
'万签插架' => '萬籤插架',
'万扎' => '萬紮',
'万象' => '萬象',
'万只' => '萬隻',
+'万余' => '萬餘',
'落后' => '落後',
-'落于' => '落於',
+'落腮胡' => '落腮鬍',
'落发' => '落髮',
+'叶叶琹' => '葉叶琹',
'着儿' => '著兒',
+'着克制' => '著剋制',
'着书立说' => '著書立說',
'着色软体' => '著色軟體',
'着重指出' => '著重指出',
@@ -7128,11 +7774,12 @@ $zh2Hant = array(
'蒙雾露' => '蒙霧露',
'蒜发' => '蒜髮',
'苍术' => '蒼朮',
+'苍发' => '蒼髮',
'苍郁' => '蒼鬱',
'蓄发' => '蓄髮',
+'蓄胡' => '蓄鬍',
'蓄须' => '蓄鬚',
'蓊郁' => '蓊鬱',
-'盖于' => '蓋於',
'蓬蓬松松' => '蓬蓬鬆鬆',
'蓬发' => '蓬髮',
'蓬松' => '蓬鬆',
@@ -7171,6 +7818,9 @@ $zh2Hant = array(
'姜黄' => '薑黃',
'薙发' => '薙髮',
'薝卜' => '薝蔔',
+'苧悴' => '薴悴',
+'苧烯' => '薴烯',
+'薴烯' => '薴烯',
'借以' => '藉以',
'借助' => '藉助',
'借寇兵' => '藉寇兵',
@@ -7181,6 +7831,7 @@ $zh2Hant = array(
'借箸代筹' => '藉箸代籌',
'借着' => '藉著',
'借资' => '藉資',
+'蓝淀' => '藍澱',
'藏于' => '藏於',
'藏历' => '藏曆',
'藏历史' => '藏歷史',
@@ -7257,7 +7908,6 @@ $zh2Hant = array(
'萝卜干' => '蘿蔔乾',
'虎须' => '虎鬚',
'虎斗' => '虎鬥',
-'处于' => '處於',
'号志' => '號誌',
'虫部' => '虫部',
'蚊动牛斗' => '蚊動牛鬥',
@@ -7268,6 +7918,8 @@ $zh2Hant = array(
'蜜里调油' => '蜜裡調油',
'蜡月' => '蜡月',
'蜡祭' => '蜡祭',
+'蝎蝎螫螫' => '蝎蝎螫螫',
+'蝎谮' => '蝎譖',
'虮蝨相吊' => '蟣蝨相弔',
'蛏干' => '蟶乾',
'蠁干' => '蠁幹',
@@ -7283,6 +7935,7 @@ $zh2Hant = array(
'行于' => '行於',
'行百里者半于九十' => '行百里者半於九十',
'胡同' => '衚衕',
+'卫星钟' => '衛星鐘',
'冲上' => '衝上',
'冲下' => '衝下',
'冲来' => '衝來',
@@ -7336,6 +7989,7 @@ $zh2Hant = array(
'表面' => '表面',
'衷于' => '衷於',
'袋里' => '袋裡',
+'袋表' => '袋錶',
'袖里' => '袖裡',
'被里' => '被裡',
'被复' => '被複',
@@ -7347,15 +8001,14 @@ $zh2Hant = array(
'被发阳狂' => '被髮陽狂',
'裁并' => '裁併',
'裁制' => '裁製',
-'里勾外连' => '裏勾外連',
'里手' => '裏手',
'里海' => '裏海',
-'里面' => '裏面',
'补于' => '補於',
'补药' => '補藥',
'补血药' => '補血藥',
'补注' => '補註',
'装折' => '裝摺',
+'里勾外连' => '裡勾外連',
'里外' => '裡外',
'里子' => '裡子',
'里屋' => '裡屋',
@@ -7370,7 +8023,7 @@ $zh2Hant = array(
'里通外敌' => '裡通外敵',
'里边' => '裡邊',
'里间' => '裡間',
-'里面儿' => '裡面兒',
+'里面' => '裡面',
'里面包' => '裡面包',
'里头' => '裡頭',
'制件' => '製件',
@@ -7380,8 +8033,10 @@ $zh2Hant = array(
'制冰' => '製冰',
'制冷' => '製冷',
'制剂' => '製劑',
+'制取' => '製取',
'制品' => '製品',
'制图' => '製圖',
+'制得' => '製得',
'制成' => '製成',
'制法' => '製法',
'制浆' => '製漿',
@@ -7405,11 +8060,10 @@ $zh2Hant = array(
'复函数' => '複函數',
'复分数' => '複分數',
'复分析' => '複分析',
-'复分解反应' => '複分解反應',
+'复分解' => '複分解',
'复列' => '複列',
'复利' => '複利',
'复印' => '複印',
-'复原' => '複原',
'复句' => '複句',
'复合' => '複合',
'复名' => '複名',
@@ -7420,6 +8074,7 @@ $zh2Hant = array(
'复字键' => '複字鍵',
'复审' => '複審',
'复写' => '複寫',
+'复对数' => '複對數',
'复平面' => '複平面',
'复式' => '複式',
'复复' => '複復',
@@ -7464,14 +8119,17 @@ $zh2Hant = array(
'衬里' => '襯裡',
'西占' => '西佔',
'西元后' => '西元後',
+'西周钟' => '西周鐘',
'西岳' => '西嶽',
'西晒' => '西晒',
'西历' => '西曆',
'西历史' => '西歷史',
+'西米谷' => '西米谷',
'西药' => '西藥',
'西谷米' => '西谷米',
'西游' => '西遊',
'要占' => '要佔',
+'要克制' => '要剋制',
'要占卜' => '要占卜',
'要自制' => '要自制',
'要冲' => '要衝',
@@ -7508,9 +8166,9 @@ $zh2Hant = array(
'言云' => '言云',
'言大而夸' => '言大而夸',
'言辩而确' => '言辯而确',
-'订于' => '訂於',
'订制' => '訂製',
'计划' => '計劃',
+'计时表' => '計時錶',
'托了' => '託了',
'托事' => '託事',
'托交' => '託交',
@@ -7528,7 +8186,6 @@ $zh2Hant = array(
'托故' => '託故',
'托疾' => '託疾',
'托病' => '託病',
-'托福' => '託福',
'托管' => '託管',
'托言' => '託言',
'托词' => '託詞',
@@ -7539,7 +8196,6 @@ $zh2Hant = array(
'托运' => '託運',
'托过' => '託過',
'托附' => '託附',
-'设于' => '設於',
'许愿起经' => '許愿起經',
'诉说着' => '訴說著',
'注上' => '註上',
@@ -7554,8 +8210,9 @@ $zh2Hant = array(
'注解' => '註解',
'注记' => '註記',
'注译' => '註譯',
-'注释' => '註釋',
'注销' => '註銷',
+'注:' => '註:',
+'评断发' => '評斷發',
'评注' => '評註',
'词干' => '詞幹',
'词汇' => '詞彙',
@@ -7565,11 +8222,12 @@ $zh2Hant = array(
'试药' => '試藥',
'试制' => '試製',
'诗云' => '詩云',
+'詩云' => '詩云',
'诗赞' => '詩讚',
'诗钟' => '詩鐘',
'诗余' => '詩餘',
'话里有话' => '話裡有話',
-'该于' => '該於',
+'该钟' => '該鐘',
'详征博引' => '詳徵博引',
'详注' => '詳註',
'诔赞' => '誄讚',
@@ -7585,15 +8243,19 @@ $zh2Hant = array(
'语云' => '語云',
'语汇' => '語彙',
'语有云' => '語有云',
+'語有云' => '語有云',
'诚征' => '誠徵',
'诚朴' => '誠樸',
'诬蔑' => '誣衊',
'说着' => '說著',
+'谁干的' => '誰幹的',
'课后' => '課後',
'课征' => '課徵',
'课余' => '課餘',
'调准' => '調準',
'调制' => '調製',
+'调表' => '調錶',
+'调钟表' => '調鐘錶',
'谈征' => '談徵',
'请参阅' => '請參閱',
'请君入瓮' => '請君入甕',
@@ -7607,14 +8269,17 @@ $zh2Hant = array(
'谬赞' => '謬讚',
'謷丑' => '謷醜',
'谨于心' => '謹於心',
-'证于' => '證於',
'警世钟' => '警世鐘',
+'警报钟' => '警報鐘',
+'警示钟' => '警示鐘',
'警钟' => '警鐘',
'译注' => '譯註',
'护发' => '護髮',
'读后' => '讀後',
'变征' => '變徵',
'变丑' => '變醜',
+'变脏' => '變髒',
+'变髒' => '變髒',
'仇問' => '讎問',
'仇夷' => '讎夷',
'仇校' => '讎校',
@@ -7629,7 +8294,7 @@ $zh2Hant = array(
'赞歌' => '讚歌',
'赞叹' => '讚歎',
'赞美' => '讚美',
-'赞羨' => '讚羨',
+'赞羡' => '讚羨',
'赞许' => '讚許',
'赞词' => '讚詞',
'赞誉' => '讚譽',
@@ -7651,33 +8316,34 @@ $zh2Hant = array(
'贵征' => '貴徵',
'買凶' => '買兇',
'买凶' => '買兇',
+'买断发' => '買斷發',
'费占' => '費佔',
'贻范' => '貽範',
'资金占用' => '資金占用',
'贾后' => '賈後',
'赈饥' => '賑饑',
'赏赞' => '賞讚',
+'卖断发' => '賣斷發',
'卖呆' => '賣獃',
'质朴' => '質樸',
'赌台' => '賭檯',
'赌斗' => '賭鬥',
-'赖于' => '賴於',
'賸余' => '賸餘',
'购并' => '購併',
'购买欲' => '購買慾',
'赢余' => '贏餘',
+'赤术' => '赤朮',
'赤绳系足' => '赤繩繫足',
'赤霉素' => '赤霉素',
'走回路' => '走回路',
'走后' => '走後',
-'起于' => '起於',
'起复' => '起複',
'起哄' => '起鬨',
'超级杯' => '超級盃',
'赶制' => '趕製',
'赶面棍' => '趕麵棍',
+'赵治勋' => '趙治勳',
'赵庄' => '趙莊',
-'趋于' => '趨於',
'趱干' => '趲幹',
'足于' => '足於',
'跌扑' => '跌扑',
@@ -7698,9 +8364,9 @@ $zh2Hant = array(
'车站里' => '車站裡',
'车里' => '車裡',
'轨范' => '軌範',
+'军队克制' => '軍隊剋制',
'轩辟' => '軒闢',
'较于' => '較於',
-'载于' => '載於',
'挽曲' => '輓曲',
'挽歌' => '輓歌',
'挽聯' => '輓聯',
@@ -7731,7 +8397,6 @@ $zh2Hant = array(
'农庄' => '農莊',
'农药' => '農藥',
'迂回' => '迂迴',
-'近于' => '近於',
'近日無仇' => '近日無讎',
'近日里' => '近日裡',
'返朴' => '返樸',
@@ -7769,16 +8434,15 @@ $zh2Hant = array(
'退后' => '退後',
'退烧药' => '退燒藥',
'退藏于密' => '退藏於密',
+'逆钟' => '逆鐘',
+'逆钟向' => '逆鐘向',
'逋发' => '逋髮',
'逍遥游' => '逍遙遊',
'透辟' => '透闢',
+'这只是' => '這只是',
'这伙人' => '這夥人',
-'这里' => '這裏',
-'这里在' => '這裡在',
-'这里是' => '這裡是',
-'这里会' => '這裡會',
-'这里有' => '這裡有',
-'这里能' => '這裡能',
+'这里' => '這裡',
+'这钟' => '這鐘',
'这只' => '這隻',
'这么' => '這麼',
'这么着' => '這麼著',
@@ -7790,11 +8454,12 @@ $zh2Hant = array(
'通庄' => '通莊',
'逞凶鬥狠' => '逞兇鬥狠',
'逞凶斗狠' => '逞兇鬥狠',
+'造钟' => '造鐘',
+'造钟表' => '造鐘錶',
'造曲' => '造麯',
'连三并四' => '連三併四',
'连占' => '連佔',
'连采' => '連採',
-'连于' => '連於',
'连系' => '連繫',
'连庄' => '連莊',
'周游世界' => '週遊世界',
@@ -7825,7 +8490,6 @@ $zh2Hant = array(
'游历' => '遊歷',
'游民' => '遊民',
'游河' => '遊河',
-'游牧' => '遊牧',
'游猎' => '遊獵',
'游玩' => '遊玩',
'游荡' => '遊盪',
@@ -7850,7 +8514,6 @@ $zh2Hant = array(
'游离' => '遊離',
'游骑兵' => '遊騎兵',
'游魂' => '遊魂',
-'遍于' => '遍於',
'过后' => '過後',
'过于' => '過於',
'过杆' => '過杆',
@@ -7858,22 +8521,24 @@ $zh2Hant = array(
'道范' => '道範',
'逊于' => '遜於',
'递回' => '遞迴',
-'远于' => '遠於',
'远县才至' => '遠縣纔至',
'远游' => '遠遊',
'遨游' => '遨遊',
-'适于' => '適於',
'遮丑' => '遮醜',
'迁于' => '遷於',
+'选手表明' => '選手表明',
+'选手表决' => '選手表決',
+'选手表现' => '選手表現',
+'选手表示' => '選手表示',
+'选手表达' => '選手表達',
+'遗传钟' => '遺傳鐘',
'遗范' => '遺範',
'遗迹' => '遺迹',
'辽沈' => '遼瀋',
'避孕药' => '避孕藥',
-'避暑山庄' => '避暑山庄',
'邀天之幸' => '邀天之倖',
'还占' => '還佔',
'还采' => '還採',
-'还于' => '還於',
'还冲' => '還衝',
'邋里邋遢' => '邋裡邋遢',
'那只是' => '那只是',
@@ -7963,16 +8628,18 @@ $zh2Hant = array(
'丑类' => '醜類',
'酝酿着' => '醞釀著',
'医药' => '醫藥',
+'医院里' => '醫院裡',
'酿制' => '釀製',
'衅钟' => '釁鐘',
'采石之役' => '采石之役',
-'采石之戰' => '采石之戰',
'采石之战' => '采石之戰',
+'采石之戰' => '采石之戰',
'采石磯' => '采石磯',
'采石矶' => '采石磯',
'釉药' => '釉藥',
'里程表' => '里程錶',
'重划' => '重劃',
+'重回' => '重回',
'重折' => '重摺',
'重于' => '重於',
'重罗面' => '重羅麵',
@@ -7992,6 +8659,7 @@ $zh2Hant = array(
'金仆姑' => '金僕姑',
'金仑溪' => '金崙溪',
'金布道' => '金布道',
+'金范' => '金範',
'金表情' => '金表情',
'金表态' => '金表態',
'金表扬' => '金表揚',
@@ -8008,30 +8676,44 @@ $zh2Hant = array(
'金马仑道' => '金馬崙道',
'金发' => '金髮',
'钉锤' => '釘鎚',
+'钩心斗角' => '鈎心鬥角',
'铃响后' => '鈴響後',
-'钩心斗角' => '鉤心鬥角',
'银朱' => '銀硃',
'银发' => '銀髮',
+'铜范' => '銅範',
'铜制' => '銅製',
'铜钟' => '銅鐘',
+'铯钟' => '銫鐘',
'铝制' => '鋁製',
'铺锦列绣' => '鋪錦列繡',
+'钢之炼金术师' => '鋼之鍊金術師',
'钢梁' => '鋼樑',
'钢制' => '鋼製',
'录着' => '錄著',
'录制' => '錄製',
'锤炼' => '錘鍊',
'钱谷' => '錢穀',
+'钱范' => '錢範',
'钱庄' => '錢莊',
'锦绣花园' => '錦綉花園',
'锦绣' => '錦繡',
+'表停' => '錶停',
+'表冠' => '錶冠',
'表带' => '錶帶',
'表店' => '錶店',
'表厂' => '錶廠',
+'表快' => '錶快',
+'表慢' => '錶慢',
'表板' => '錶板',
'表壳' => '錶殼',
+'表王' => '錶王',
+'表的嘀嗒' => '錶的嘀嗒',
+'表的历史' => '錶的歷史',
'表盘' => '錶盤',
'表蒙子' => '錶蒙子',
+'表行' => '錶行',
+'表转' => '錶轉',
+'表速' => '錶速',
'表针' => '錶針',
'表链' => '錶鏈',
'炼冶' => '鍊冶',
@@ -8044,12 +8726,11 @@ $zh2Hant = array(
'炼汞' => '鍊汞',
'炼石' => '鍊石',
'炼贫' => '鍊貧',
-'炼金' => '鍊金',
+'炼金术' => '鍊金術',
'炼钢' => '鍊鋼',
'锅庄' => '鍋莊',
'锻炼出' => '鍛鍊出',
'锲而不舍' => '鍥而不捨',
-'钟表' => '鍾錶',
'镰仓' => '鎌倉',
'锤儿' => '鎚兒',
'锤子' => '鎚子',
@@ -8057,25 +8738,76 @@ $zh2Hant = array(
'锈病' => '鏽病',
'锈菌' => '鏽菌',
'锈蚀' => '鏽蝕',
+'钟上' => '鐘上',
+'钟下' => '鐘下',
+'钟不' => '鐘不',
'钟不扣不鸣' => '鐘不扣不鳴',
'钟不撞不鸣' => '鐘不撞不鳴',
+'钟不敲不响' => '鐘不敲不響',
+'钟不空则哑' => '鐘不空則啞',
'钟乳洞' => '鐘乳洞',
'钟乳石' => '鐘乳石',
-'钟在寺里' => '鐘在寺里',
+'钟停' => '鐘停',
+'钟匠' => '鐘匠',
+'钟口' => '鐘口',
+'钟在寺里' => '鐘在寺裡',
'钟塔' => '鐘塔',
+'钟壁' => '鐘壁',
+'钟太' => '鐘太',
+'钟好' => '鐘好',
'钟山' => '鐘山',
+'钟左右' => '鐘左右',
+'钟差' => '鐘差',
+'钟座' => '鐘座',
+'钟形' => '鐘形',
'钟形虫' => '鐘形蟲',
+'钟律' => '鐘律',
+'钟快' => '鐘快',
+'钟意' => '鐘意',
+'钟慢' => '鐘慢',
'钟摆' => '鐘擺',
+'钟敲' => '鐘敲',
+'钟有' => '鐘有',
'钟楼' => '鐘樓',
+'钟模' => '鐘模',
+'钟没' => '鐘沒',
'钟漏' => '鐘漏',
+'钟王' => '鐘王',
'钟琴' => '鐘琴',
+'钟发音' => '鐘發音',
+'钟的' => '鐘的',
+'钟盘' => '鐘盤',
'钟相' => '鐘相',
'钟磬' => '鐘磬',
+'钟纽' => '鐘紐',
+'钟罩' => '鐘罩',
'钟声' => '鐘聲',
-'钟表店' => '鐘錶店',
+'钟腰' => '鐘腰',
+'钟螺' => '鐘螺',
+'钟行' => '鐘行',
+'钟表面' => '鐘表面',
+'钟被' => '鐘被',
+'钟调' => '鐘調',
+'钟身' => '鐘身',
+'钟速' => '鐘速',
+'钟表' => '鐘錶',
+'钟表停' => '鐘錶停',
+'钟表快' => '鐘錶快',
+'钟表慢' => '鐘錶慢',
+'钟表历史' => '鐘錶歷史',
+'钟表王' => '鐘錶王',
+'钟表的' => '鐘錶的',
+'钟表的历史' => '鐘錶的歷史',
+'钟表盘' => '鐘錶盤',
+'钟表行' => '鐘錶行',
+'钟表速' => '鐘錶速',
'钟关' => '鐘關',
+'钟陈列' => '鐘陳列',
+'钟面' => '鐘面',
'钟响' => '鐘響',
+'钟顶' => '鐘頂',
'钟头' => '鐘頭',
+'钟体' => '鐘體',
'钟鸣' => '鐘鳴',
'钟点' => '鐘點',
'钟鼎' => '鐘鼎',
@@ -8084,6 +8816,7 @@ $zh2Hant = array(
'铁栏杆' => '鐵欄杆',
'铁锤' => '鐵鎚',
'铁锈' => '鐵鏽',
+'铁钟' => '鐵鐘',
'铸钟' => '鑄鐘',
'鉴于' => '鑒於',
'长几' => '長几',
@@ -8091,13 +8824,16 @@ $zh2Hant = array(
'长历' => '長曆',
'长历史' => '長歷史',
'长生药' => '長生藥',
+'长胡' => '長鬍',
'门前门后' => '門前門後',
'门帘' => '門帘',
'门吊儿' => '門弔兒',
'门里' => '門裡',
+'闫怀礼' => '閆懷禮',
'开吊' => '開弔',
'开征' => '開徵',
'开采' => '開採',
+'开发' => '開發',
'开药' => '開藥',
'开辟' => '開闢',
'开哄' => '開鬨',
@@ -8128,20 +8864,20 @@ $zh2Hant = array(
'辟谣' => '闢謠',
'辟辟' => '闢辟',
'辟邪以律' => '闢邪以律',
-'防患于未然' => '防患於未然',
'防晒' => '防晒',
+'防水表' => '防水錶',
'防御' => '防禦',
'防范' => '防範',
'防锈' => '防鏽',
'防台' => '防颱',
'阻于' => '阻於',
'阿呆瓜' => '阿呆瓜',
+'阿斯图里亚斯' => '阿斯圖里亞斯',
'阿呆' => '阿獃',
'附于' => '附於',
'附注' => '附註',
'降压药' => '降壓藥',
-'降于' => '降於',
-'限于' => '限於',
+'限制' => '限制',
'升官' => '陞官',
'除臭药' => '除臭藥',
'陪吊' => '陪弔',
@@ -8151,7 +8887,6 @@ $zh2Hant = array(
'阴沟里翻船' => '陰溝裡翻船',
'阴郁' => '陰鬱',
'陈炼' => '陳鍊',
-'陷于' => '陷於',
'陆游' => '陸遊',
'阳春面' => '陽春麵',
'阳历' => '陽曆',
@@ -8175,8 +8910,8 @@ $zh2Hant = array(
'集于' => '集於',
'集游法' => '集遊法',
'雇佣' => '雇傭',
-'雇于' => '雇於',
'雕梁画栋' => '雕樑畫棟',
+'双折射' => '雙折射',
'双折' => '雙摺',
'双胜类' => '雙胜類',
'双雕' => '雙鵰',
@@ -8197,24 +8932,28 @@ $zh2Hant = array(
'雨后' => '雨後',
'雪窗萤几' => '雪窗螢几',
'雪里' => '雪裡',
-'雪里红' => '雪里紅',
+'雪里红' => '雪裡紅',
+'雪里蕻' => '雪裡蕻',
'云南白药' => '雲南白藥',
'云笈七签' => '雲笈七籤',
'云游' => '雲遊',
'云须' => '雲鬚',
+'零个' => '零個',
'零多只' => '零多隻',
'零天后' => '零天後',
'零只' => '零隻',
'零余' => '零餘',
+'电子表格' => '電子表格',
'电子表' => '電子錶',
'电子钟' => '電子鐘',
+'电子钟表' => '電子鐘錶',
'电杆' => '電杆',
+'电码表' => '電碼表',
'电线杆' => '電線杆',
'电冲' => '電衝',
'电表' => '電錶',
'电钟' => '電鐘',
'震栗' => '震慄',
-'震于' => '震於',
'震荡' => '震蕩',
'雾里' => '霧裡',
'露丑' => '露醜',
@@ -8228,9 +8967,7 @@ $zh2Hant = array(
'青霉素' => '青霉素',
'青霉' => '青黴',
'非占不可' => '非佔不可',
-'非于' => '非於',
'靠后' => '靠後',
-'靠里面' => '靠裡面',
'面包住' => '面包住',
'面包含' => '面包含',
'面包围' => '面包圍',
@@ -8268,14 +9005,20 @@ $zh2Hant = array(
'韩制' => '韓製',
'音准' => '音準',
'音声如钟' => '音聲如鐘',
-'韶山冲' => '韶山衝',
-'页面' => '頁面',
+'韶山冲' => '韶山沖',
+'响钟' => '響鐘',
'頁面' => '頁面',
+'页面' => '頁面',
+'頂多' => '頂多',
+'顶多' => '頂多',
'项庄' => '項莊',
'顺于' => '順於',
+'顺钟向' => '順鐘向',
+'须根据' => '須根據',
'颂系' => '頌繫',
'颂赞' => '頌讚',
'预制' => '預製',
+'领域里' => '領域裡',
'领袖欲' => '領袖慾',
'头巾吊在水里' => '頭巾弔在水裡',
'头里' => '頭裡',
@@ -8291,7 +9034,10 @@ $zh2Hant = array(
'颠颠仆仆' => '顛顛仆仆',
'顾前不顾后' => '顧前不顧後',
'颤栗' => '顫慄',
-'显着标志' => '顯著標志',
+'显示表' => '顯示錶',
+'显示钟' => '顯示鐘',
+'显示钟表' => '顯示鐘錶',
+'显著标志' => '顯著標志',
'风干' => '風乾',
'风土志' => '風土誌',
'风卷残云' => '風捲殘雲',
@@ -8299,6 +9045,8 @@ $zh2Hant = array(
'风范' => '風範',
'风里' => '風裡',
'风起云涌' => '風起雲湧',
+'风采' => '風采',
+'風采' => '風采',
'台风' => '颱風',
'刮了' => '颳了',
'刮倒' => '颳倒',
@@ -8313,6 +9061,7 @@ $zh2Hant = array(
'飘飘荡荡' => '飄飄蕩蕩',
'飞扎' => '飛紮',
'飞刍挽粟' => '飛芻輓粟',
+'飞行钟' => '飛行鐘',
'食欲' => '食慾',
'食欲不振' => '食欲不振',
'食野之苹' => '食野之苹',
@@ -8404,7 +9153,7 @@ $zh2Hant = array(
'余绪' => '餘緒',
'余缺' => '餘缺',
'余罪' => '餘罪',
-'余羨' => '餘羨',
+'余羡' => '餘羨',
'余声' => '餘聲',
'余膏' => '餘膏',
'余兴' => '餘興',
@@ -8445,6 +9194,7 @@ $zh2Hant = array(
'馆后一街' => '館後一街',
'馆后二街' => '館後二街',
'馆谷' => '館穀',
+'馆里' => '館裡',
'喂乳' => '餵乳',
'喂了' => '餵了',
'喂奶' => '餵奶',
@@ -8464,12 +9214,17 @@ $zh2Hant = array(
'饥民' => '饑民',
'饥渴' => '饑渴',
'饥溺' => '饑溺',
+'饥荒' => '饑荒',
'饥饱' => '饑飽',
'饥馑' => '饑饉',
'首当其冲' => '首當其衝',
+'首发' => '首發',
+'首只' => '首隻',
'香干' => '香乾',
'香山庄' => '香山庄',
'马干' => '馬乾',
+'马占山' => '馬占山',
+'馬占山' => '馬占山',
'马后' => '馬後',
'马杆' => '馬杆',
'马表' => '馬錶',
@@ -8477,6 +9232,7 @@ $zh2Hant = array(
'骀荡' => '駘蕩',
'腾冲' => '騰衝',
'惊赞' => '驚讚',
+'惊钟' => '驚鐘',
'骨子里' => '骨子裡',
'骨干' => '骨幹',
'骨灰坛' => '骨灰罈',
@@ -8496,15 +9252,16 @@ $zh2Hant = array(
'脏词' => '髒詞',
'脏话' => '髒話',
'脏钱' => '髒錢',
+'脏发' => '髒髮',
'体范' => '體範',
+'体系' => '體系',
'高几' => '高几',
'高干扰' => '高干擾',
'高干预' => '高干預',
'高干' => '高幹',
'高度自制' => '高度自制',
-'高于' => '高於',
-'高升' => '高陞',
'髡发' => '髡髮',
+'髭胡' => '髭鬍',
'髭须' => '髭鬚',
'发上指冠' => '髮上指冠',
'发上冲冠' => '髮上沖冠',
@@ -8516,6 +9273,7 @@ $zh2Hant = array(
'发妻' => '髮妻',
'发姐' => '髮姐',
'发屋' => '髮屋',
+'发已霜白' => '髮已霜白',
'发带' => '髮帶',
'发廊' => '髮廊',
'发式' => '髮式',
@@ -8525,6 +9283,7 @@ $zh2Hant = array(
'发根' => '髮根',
'发油' => '髮油',
'发漂' => '髮漂',
+'发为血之本' => '髮為血之本',
'发状' => '髮狀',
'发癣' => '髮癬',
'发短心长' => '髮短心長',
@@ -8550,14 +9309,17 @@ $zh2Hant = array(
'发饰' => '髮飾',
'发髻' => '髮髻',
'发鬓' => '髮鬢',
+'髯胡' => '髯鬍',
'髼松' => '髼鬆',
'鬅松' => '鬅鬆',
'松一口气' => '鬆一口氣',
'松了' => '鬆了',
'松些' => '鬆些',
+'松元音' => '鬆元音',
'松劲' => '鬆勁',
'松动' => '鬆動',
'松口' => '鬆口',
+'松喉' => '鬆喉',
'松土' => '鬆土',
'松宽' => '鬆寬',
'松弛' => '鬆弛',
@@ -8586,6 +9348,7 @@ $zh2Hant = array(
'胡梢' => '鬍梢',
'胡渣' => '鬍渣',
'胡髭' => '鬍髭',
+'胡髯' => '鬍髯',
'胡须' => '鬍鬚',
'鬒发' => '鬒髮',
'须根' => '鬚根',
@@ -8593,6 +9356,7 @@ $zh2Hant = array(
'须生' => '鬚生',
'须眉' => '鬚眉',
'须发' => '鬚髮',
+'须胡' => '鬚鬍',
'须须' => '鬚鬚',
'须鲨' => '鬚鯊',
'须鲸' => '鬚鯨',
@@ -8609,6 +9373,7 @@ $zh2Hant = array(
'斗口' => '鬥口',
'斗合' => '鬥合',
'斗嘴' => '鬥嘴',
+'斗地主' => '鬥地主',
'斗士' => '鬥士',
'斗富' => '鬥富',
'斗巧' => '鬥巧',
@@ -8671,6 +9436,7 @@ $zh2Hant = array(
'斗鹌鹑' => '鬥鵪鶉',
'斗丽' => '鬥麗',
'闹着玩儿' => '鬧著玩兒',
+'闹表' => '鬧錶',
'闹钟' => '鬧鐘',
'哄动' => '鬨動',
'哄堂' => '鬨堂',
@@ -8678,6 +9444,7 @@ $zh2Hant = array(
'郁伊' => '鬱伊',
'郁勃' => '鬱勃',
'郁卒' => '鬱卒',
+'郁南' => '鬱南',
'郁堙不偶' => '鬱堙不偶',
'郁塞' => '鬱塞',
'郁垒' => '鬱壘',
@@ -8687,6 +9454,7 @@ $zh2Hant = array(
'郁愤' => '鬱憤',
'郁抑' => '鬱抑',
'郁挹' => '鬱挹',
+'郁林' => '鬱林',
'郁气' => '鬱氣',
'郁江' => '鬱江',
'郁沉沉' => '鬱沉沉',
@@ -8715,11 +9483,13 @@ $zh2Hant = array(
'鬼谷子' => '鬼谷子',
'魂牵梦系' => '魂牽夢繫',
'魏征' => '魏徵',
+'魔表' => '魔錶',
'鱼干' => '魚乾',
'鱼松' => '魚鬆',
'鲸须' => '鯨鬚',
'鲇鱼' => '鯰魚',
'鸠占鹊巢' => '鳩佔鵲巢',
+'凤凰于飞' => '鳳凰于飛',
'凤梨干' => '鳳梨乾',
'鸣钟' => '鳴鐘',
'鸿案相庄' => '鴻案相莊',
@@ -8733,6 +9503,7 @@ $zh2Hant = array(
'雕鹗' => '鵰鶚',
'鹤吊' => '鶴弔',
'鹤发' => '鶴髮',
+'鹰雕' => '鹰鵰',
'咸味' => '鹹味',
'咸嘴淡舌' => '鹹嘴淡舌',
'咸土' => '鹹土',
@@ -8816,6 +9587,9 @@ $zh2Hant = array(
'黄曲毒素' => '黃麴毒素',
'黑奴吁天录' => '黑奴籲天錄',
'黑发' => '黑髮',
+'点半钟' => '點半鐘',
+'点多钟' => '點多鐘',
+'点里' => '點裡',
'点钟' => '點鐘',
'霉毒' => '黴毒',
'霉素' => '黴素',
@@ -8848,34 +9622,50 @@ $zh2Hant = array(
'龙须' => '龍鬚',
'龙斗虎伤' => '龍鬥虎傷',
'龟山庄' => '龜山庄',
+'!克制' => '!剋制',
+',克制' => ',剋制',
'0多只' => '0多隻',
'0天后' => '0天後',
'0只' => '0隻',
'0余' => '0餘',
'1天后' => '1天後',
'1只' => '1隻',
+'1余' => '1餘',
'2天后' => '2天後',
'2只' => '2隻',
+'2余' => '2餘',
'3天后' => '3天後',
'3只' => '3隻',
+'3余' => '3餘',
'4天后' => '4天後',
'4只' => '4隻',
+'4余' => '4餘',
'5天后' => '5天後',
'5只' => '5隻',
+'5余' => '5餘',
'6天后' => '6天後',
'6只' => '6隻',
+'6余' => '6餘',
'7天后' => '7天後',
'7只' => '7隻',
+'7余' => '7餘',
'8天后' => '8天後',
'8只' => '8隻',
+'8余' => '8餘',
'9天后' => '9天後',
'9只' => '9隻',
+'9余' => '9餘',
+':克制' => ':剋制',
+';克制' => ';剋制',
+'?克制' => '?剋制',
);
$zh2Hans = array(
'㑳' => '㑇',
+'㞞' => '𪨊',
'㠏' => '㟆',
'㩜' => '㨫',
+'䉬' => '𫂈',
'䊷' => '䌶',
'䋙' => '䌺',
'䋻' => '䌾',
@@ -8984,6 +9774,7 @@ $zh2Hans = array(
'冪' => '幂',
'凈' => '净',
'凍' => '冻',
+'凙' => '𪞝',
'凜' => '凛',
'凱' => '凯',
'別' => '别',
@@ -9065,6 +9856,7 @@ $zh2Hans = array(
'嗩' => '唢',
'嗰' => '𠮶',
'嗶' => '哔',
+'嗹' => '𪡏',
'嘆' => '叹',
'嘍' => '喽',
'嘔' => '呕',
@@ -9119,6 +9911,7 @@ $zh2Hans = array(
'圓' => '圆',
'圖' => '图',
'團' => '团',
+'圞' => '𪢮',
'垵' => '埯',
'埡' => '垭',
'埰' => '采',
@@ -9227,6 +10020,7 @@ $zh2Hans = array(
'屢' => '屡',
'層' => '层',
'屨' => '屦',
+'屩' => '𪨗',
'屬' => '属',
'岡' => '冈',
'峴' => '岘',
@@ -9257,7 +10051,9 @@ $zh2Hans = array(
'巋' => '岿',
'巒' => '峦',
'巔' => '巅',
+'巖' => '岩',
'巰' => '巯',
+'巹' => '卺',
'帥' => '帅',
'師' => '师',
'帳' => '帐',
@@ -9804,6 +10600,7 @@ $zh2Hans = array(
'瑩' => '莹',
'瑪' => '玛',
'瑲' => '玱',
+'瑽' => '𪻐',
'璉' => '琏',
'璣' => '玑',
'璦' => '瑷',
@@ -9875,6 +10672,7 @@ $zh2Hans = array(
'盪' => '荡',
'眥' => '眦',
'眾' => '众',
+'睍' => '𪾢',
'睏' => '困',
'睜' => '睁',
'睞' => '睐',
@@ -10059,6 +10857,7 @@ $zh2Hans = array(
'絳' => '绛',
'絶' => '绝',
'絹' => '绢',
+'絺' => '𫄨',
'綁' => '绑',
'綃' => '绡',
'綆' => '绠',
@@ -10170,6 +10969,7 @@ $zh2Hans = array(
'繽' => '缤',
'繾' => '缱',
'繿' => '䍀',
+'纁' => '𫄸',
'纈' => '缬',
'纊' => '纩',
'續' => '续',
@@ -10192,6 +10992,7 @@ $zh2Hans = array(
'羈' => '羁',
'羋' => '芈',
'羥' => '羟',
+'羨' => '羡',
'義' => '义',
'習' => '习',
'翹' => '翘',
@@ -10245,6 +11046,7 @@ $zh2Hans = array(
'興' => '兴',
'舉' => '举',
'舊' => '旧',
+'舘' => '馆',
'艙' => '舱',
'艤' => '舣',
'艦' => '舰',
@@ -10364,6 +11166,7 @@ $zh2Hans = array(
'蟻' => '蚁',
'蠅' => '蝇',
'蠆' => '虿',
+'蠍' => '蝎',
'蠐' => '蛴',
'蠑' => '蝾',
'蠟' => '蜡',
@@ -10394,6 +11197,8 @@ $zh2Hans = array(
'褳' => '裢',
'褸' => '褛',
'褻' => '亵',
+'襀' => '𫌀',
+'襆' => '幞',
'襇' => '裥',
'襏' => '袯',
'襖' => '袄',
@@ -10419,6 +11224,7 @@ $zh2Hans = array(
'覲' => '觐',
'覷' => '觑',
'覺' => '觉',
+'覼' => '𫌨',
'覽' => '览',
'覿' => '觌',
'觀' => '观',
@@ -10433,6 +11239,7 @@ $zh2Hans = array(
'訌' => '讧',
'討' => '讨',
'訐' => '讦',
+'訑' => '𫍙',
'訒' => '讱',
'訓' => '训',
'訕' => '讪',
@@ -10532,6 +11339,7 @@ $zh2Hans = array(
'諫' => '谏',
'諭' => '谕',
'諮' => '咨',
+'諰' => '𫍰',
'諱' => '讳',
'諳' => '谙',
'諶' => '谌',
@@ -10547,6 +11355,7 @@ $zh2Hans = array(
'謅' => '诌',
'謊' => '谎',
'謎' => '谜',
+'謏' => '𫍲',
'謐' => '谧',
'謔' => '谑',
'謖' => '谡',
@@ -10566,6 +11375,7 @@ $zh2Hans = array(
'謾' => '谩',
'譅' => '䜧',
'證' => '证',
+'譊' => '𫍢',
'譎' => '谲',
'譏' => '讥',
'譖' => '谮',
@@ -10689,6 +11499,7 @@ $zh2Hans = array(
'蹣' => '蹒',
'蹤' => '踪',
'蹺' => '跷',
+'蹻' => '𫏋',
'躂' => '跶',
'躉' => '趸',
'躊' => '踌',
@@ -10708,12 +11519,14 @@ $zh2Hans = array(
'軋' => '轧',
'軌' => '轨',
'軍' => '军',
+'軏' => '𫐄',
'軑' => '轪',
'軒' => '轩',
'軔' => '轫',
'軛' => '轭',
'軟' => '软',
'軤' => '轷',
+'軨' => '𫐉',
'軫' => '轸',
'軲' => '轱',
'軸' => '轴',
@@ -10732,6 +11545,7 @@ $zh2Hans = array(
'輓' => '挽',
'輔' => '辅',
'輕' => '轻',
+'輗' => '𫐐',
'輛' => '辆',
'輜' => '辎',
'輝' => '辉',
@@ -10742,6 +11556,7 @@ $zh2Hans = array(
'輩' => '辈',
'輪' => '轮',
'輬' => '辌',
+'輮' => '𫐓',
'輯' => '辑',
'輳' => '辏',
'輸' => '输',
@@ -10760,6 +11575,7 @@ $zh2Hans = array(
'轟' => '轰',
'轡' => '辔',
'轢' => '轹',
+'轣' => '𫐆',
'轤' => '轳',
'辦' => '办',
'辭' => '辞',
@@ -10816,6 +11632,7 @@ $zh2Hans = array(
'醣' => '糖',
'醫' => '医',
'醬' => '酱',
+'醯' => '酰',
'醱' => '酦',
'釀' => '酿',
'釁' => '衅',
@@ -10845,6 +11662,7 @@ $zh2Hans = array(
'鈁' => '钫',
'鈃' => '钘',
'鈄' => '钭',
+'鈇' => '𫓧',
'鈈' => '钚',
'鈉' => '钠',
'鈋' => '𨱂',
@@ -11022,6 +11840,7 @@ $zh2Hans = array(
'鎩' => '铩',
'鎪' => '锼',
'鎬' => '镐',
+'鎭' => '鎮',
'鎮' => '镇',
'鎯' => '𨱍',
'鎰' => '镒',
@@ -11050,6 +11869,7 @@ $zh2Hans = array(
'鏡' => '镜',
'鏢' => '镖',
'鏤' => '镂',
+'鏦' => '𫓩',
'鏨' => '錾',
'鏰' => '镚',
'鏵' => '铧',
@@ -11059,6 +11879,7 @@ $zh2Hans = array(
'鏽' => '锈',
'鐃' => '铙',
'鐋' => '铴',
+'鐍' => '𫔎',
'鐎' => '𨱓',
'鐏' => '𨱔',
'鐐' => '镣',
@@ -11216,6 +12037,7 @@ $zh2Hans = array(
'韓' => '韩',
'韙' => '韪',
'韜' => '韬',
+'韝' => '鞲',
'韞' => '韫',
'韻' => '韵',
'響' => '响',
@@ -11319,15 +12141,19 @@ $zh2Hans = array(
'餑' => '饽',
'餒' => '馁',
'餓' => '饿',
+'餔' => '𫗦',
'餕' => '馂',
'餖' => '饾',
+'餗' => '𫗧',
'餘' => '余',
'餚' => '肴',
'餛' => '馄',
'餜' => '馃',
'餞' => '饯',
'餡' => '馅',
+'餦' => '𫗠',
'館' => '馆',
+'餭' => '𫗮',
'餱' => '糇',
'餳' => '饧',
'餵' => '喂',
@@ -11349,6 +12175,7 @@ $zh2Hans = array(
'饑' => '饥',
'饒' => '饶',
'饗' => '飨',
+'饘' => '𫗴',
'饜' => '餍',
'饞' => '馋',
'饢' => '馕',
@@ -11360,6 +12187,7 @@ $zh2Hans = array(
'馴' => '驯',
'馹' => '驲',
'駁' => '驳',
+'駃' => '𫘝',
'駎' => '𩧨',
'駐' => '驻',
'駑' => '驽',
@@ -11381,9 +12209,11 @@ $zh2Hans = array(
'駱' => '骆',
'駶' => '𩧺',
'駸' => '骎',
+'駻' => '𫘣',
'駿' => '骏',
'騁' => '骋',
'騂' => '骍',
+'騃' => '𫘤',
'騅' => '骓',
'騌' => '骔',
'騍' => '骒',
@@ -11395,6 +12225,7 @@ $zh2Hans = array(
'騚' => '𩨊',
'騝' => '𩨃',
'騟' => '𩨈',
+'騠' => '𫘨',
'騤' => '骙',
'騧' => '䯄',
'騪' => '𩨄',
@@ -11449,6 +12280,7 @@ $zh2Hans = array(
'魘' => '魇',
'魚' => '鱼',
'魛' => '鱽',
+'魟' => '𫚉',
'魢' => '鱾',
'魥' => '𩽹',
'魨' => '鲀',
@@ -11458,6 +12290,7 @@ $zh2Hans = array(
'魺' => '鲄',
'鮁' => '鲅',
'鮃' => '鲆',
+'鮄' => '𫚒',
'鮊' => '鲌',
'鮋' => '鲉',
'鮍' => '鲏',
@@ -11478,6 +12311,7 @@ $zh2Hans = array(
'鮫' => '鲛',
'鮭' => '鲑',
'鮮' => '鲜',
+'鮰' => '𫚔',
'鮳' => '鲓',
'鮶' => '鲪',
'鮸' => '𩾃',
@@ -11485,6 +12319,7 @@ $zh2Hans = array(
'鯀' => '鲧',
'鯁' => '鲠',
'鯄' => '𩾁',
+'鯆' => '𫚙',
'鯇' => '鲩',
'鯉' => '鲤',
'鯊' => '鲨',
@@ -11525,6 +12360,7 @@ $zh2Hans = array(
'鰟' => '鳑',
'鰠' => '鳋',
'鰣' => '鲥',
+'鰤' => '𫚕',
'鰥' => '鳏',
'鰧' => '䲢',
'鰨' => '鳎',
@@ -11559,6 +12395,7 @@ $zh2Hans = array(
'鱧' => '鳢',
'鱨' => '鲿',
'鱭' => '鲚',
+'鱮' => '𫚈',
'鱯' => '鳠',
'鱷' => '鳄',
'鱸' => '鲈',
@@ -11571,13 +12408,16 @@ $zh2Hans = array(
'鳳' => '凤',
'鳴' => '鸣',
'鳶' => '鸢',
+'鳷' => '𫛛',
'鳼' => '𪉃',
'鳾' => '䴓',
+'鴃' => '𫛞',
'鴆' => '鸩',
'鴇' => '鸨',
'鴉' => '鸦',
'鴒' => '鸰',
'鴕' => '鸵',
+'鴗' => '𫁡',
'鴛' => '鸳',
'鴜' => '𪉈',
'鴝' => '鸲',
@@ -11617,8 +12457,10 @@ $zh2Hans = array(
'鶇' => '鸫',
'鶉' => '鹑',
'鶊' => '鹒',
+'鶒' => '𫛶',
'鶓' => '鹋',
'鶖' => '鹙',
+'鶗' => '𫛸',
'鶘' => '鹕',
'鶚' => '鹗',
'鶡' => '鹖',
@@ -11660,6 +12502,7 @@ $zh2Hans = array(
'鷿' => '䴙',
'鸂' => '㶉',
'鸇' => '鹯',
+'鸋' => '𫛢',
'鸌' => '鹱',
'鸏' => '鹲',
'鸕' => '鸬',
@@ -11699,6 +12542,7 @@ $zh2Hans = array(
'鼉' => '鼍',
'鼕' => '冬',
'鼴' => '鼹',
+'齇' => '齄',
'齊' => '齐',
'齋' => '斋',
'齎' => '赍',
@@ -11734,6 +12578,7 @@ $zh2Hans = array(
'𦪙' => '䑽',
'𧜵' => '䙊',
'𧝞' => '䘛',
+'𧦧' => '𫍟',
'𧩙' => '䜥',
'𧵳' => '䞌',
'𨋢' => '䢂',
@@ -11798,7 +12643,7 @@ $zh2Hans = array(
'𪇳' => '𪉕',
'𪘀' => '𪚏',
'𪘯' => '𪚐',
-'《周易乾' => '《周易乾',
+'𫚒' => '軿',
'《易乾' => '《易乾',
'不著痕跡' => '不着痕迹',
'不著邊際' => '不着边际',
@@ -11861,6 +12706,7 @@ $zh2Hans = array(
'乾仪' => '乾仪',
'乾位' => '乾位',
'乾健' => '乾健',
+'乾健也' => '乾健也',
'乾元' => '乾元',
'乾光' => '乾光',
'乾兴' => '乾兴',
@@ -11871,6 +12717,8 @@ $zh2Hans = array(
'乾刘' => '乾刘',
'乾剛' => '乾刚',
'乾刚' => '乾刚',
+'乾務' => '乾务',
+'乾务' => '乾务',
'乾化' => '乾化',
'乾卦' => '乾卦',
'乾县' => '乾县',
@@ -11887,6 +12735,7 @@ $zh2Hans = array(
'乾坤' => '乾坤',
'乾城' => '乾城',
'乾基' => '乾基',
+'乾天也' => '乾天也',
'乾始' => '乾始',
'乾姓' => '乾姓',
'乾寧' => '乾宁',
@@ -11907,6 +12756,7 @@ $zh2Hans = array(
'乾律' => '乾律',
'乾德' => '乾德',
'乾心' => '乾心',
+'乾忠' => '乾忠',
'乾文' => '乾文',
'乾斷' => '乾断',
'乾断' => '乾断',
@@ -11928,7 +12778,10 @@ $zh2Hans = array(
'乾棟' => '乾栋',
'乾步' => '乾步',
'乾氏' => '乾氏',
+'乾沓和' => '乾沓和',
+'乾沓婆' => '乾沓婆',
'乾泉' => '乾泉',
+'乾淳' => '乾淳',
'乾清宮' => '乾清宫',
'乾清宫' => '乾清宫',
'乾渥' => '乾渥',
@@ -11971,14 +12824,15 @@ $zh2Hans = array(
'乾象' => '乾象',
'乾象歷' => '乾象历',
'乾象历' => '乾象历',
-'乾貞' => '乾贞',
'乾贞' => '乾贞',
+'乾貞' => '乾贞',
'乾貺' => '乾贶',
'乾贶' => '乾贶',
'乾车' => '乾车',
'乾車' => '乾车',
'乾轴' => '乾轴',
'乾軸' => '乾轴',
+'乾通' => '乾通',
'乾造' => '乾造',
'乾道' => '乾道',
'乾鑒' => '乾鉴',
@@ -12043,6 +12897,14 @@ $zh2Hans = array(
'仰屋著書' => '仰屋著书',
'彷彿' => '仿佛',
'夥計' => '伙计',
+'傳著' => '传着',
+'傳著書' => '传著书',
+'傳著作' => '传著作',
+'傳著名' => '传著名',
+'傳著錄' => '传著录',
+'傳著稱' => '传著称',
+'傳著者' => '传著者',
+'傳著述' => '传著述',
'伴著' => '伴着',
'伴著書' => '伴著书',
'伴著作' => '伴著作',
@@ -12077,6 +12939,7 @@ $zh2Hans = array(
'側著稱' => '侧著称',
'側著者' => '侧著者',
'側著述' => '侧著述',
+'保護著' => '保护着',
'保障著' => '保障着',
'保障著書' => '保障著书',
'保障著作' => '保障著作',
@@ -12093,6 +12956,7 @@ $zh2Hans = array(
'信著稱' => '信著称',
'信著者' => '信著者',
'信著述' => '信著述',
+'修鍊' => '修炼',
'候著' => '候着',
'候著書' => '候著书',
'候著作' => '候著作',
@@ -12109,6 +12973,8 @@ $zh2Hans = array(
'藉此' => '借此',
'藉由' => '借由',
'借著' => '借着',
+'藉着' => '借着',
+'藉著' => '借着',
'藉端' => '借端',
'借著書' => '借著书',
'借著作' => '借著作',
@@ -12287,6 +13153,9 @@ $zh2Hans = array(
'叫著述' => '叫著述',
'可穿著' => '可穿著',
'叱吒' => '叱吒',
+'吃不著' => '吃不着',
+'吃得著' => '吃得着',
+'吃著' => '吃着',
'吃衣著飯' => '吃衣著饭',
'合著' => '合著',
'名著' => '名著',
@@ -12306,6 +13175,8 @@ $zh2Hans = array(
'含著稱' => '含著称',
'含著者' => '含著者',
'含著述' => '含著述',
+'聽不著' => '听不着',
+'聽得著' => '听得着',
'聽著' => '听着',
'聽著書' => '听著书',
'聽著作' => '听著作',
@@ -12324,6 +13195,7 @@ $zh2Hans = array(
'吹著稱' => '吹著称',
'吹著者' => '吹著者',
'吹著述' => '吹著述',
+'周易乾' => '周易乾',
'味著' => '味着',
'味著書' => '味著书',
'味著作' => '味著作',
@@ -12366,6 +13238,9 @@ $zh2Hans = array(
'喝著稱' => '喝著称',
'喝著者' => '喝著者',
'喝著述' => '喝著述',
+'嗅不著' => '嗅不着',
+'嗅得著' => '嗅得着',
+'嗅著' => '嗅着',
'嚷著' => '嚷着',
'嚷著書' => '嚷著书',
'嚷著作' => '嚷著作',
@@ -12515,7 +13390,10 @@ $zh2Hans = array(
'幫著稱' => '帮著称',
'幫著者' => '帮著者',
'幫著述' => '帮著述',
+'乾乾淨淨' => '干干净净',
+'乾乾脆脆' => '干干脆脆',
'乾泉水' => '干泉水',
+'幹著' => '干着',
'么二三' => '幺二三',
'幺二三' => '幺二三',
'么元' => '幺元',
@@ -12560,6 +13438,7 @@ $zh2Hans = array(
'么麼' => '幺麽',
'幺麽小丑' => '幺麽小丑',
'么麼小丑' => '幺麽小丑',
+'庇護著' => '庇护着',
'應著' => '应着',
'應著書' => '应著书',
'應著作' => '应著作',
@@ -12708,8 +13587,6 @@ $zh2Hans = array(
'想著稱' => '想著称',
'想著者' => '想著者',
'想著述' => '想著述',
-'成效顯著' => '成效显著',
-'成績顯著' => '成绩显著',
'戰著' => '战着',
'戰著書' => '战著书',
'戰著作' => '战著作',
@@ -12752,15 +13629,8 @@ $zh2Hans = array(
'扛著述' => '扛著述',
'執著' => '执著',
'找不著' => '找不着',
-'找不著書' => '找不著书',
-'找不著作' => '找不著作',
-'找不著名' => '找不著名',
-'找不著錄' => '找不著录',
-'找不著稱' => '找不著称',
-'找不著者' => '找不著者',
-'找不著述' => '找不著述',
+'找得著' => '找得着',
'抓著' => '抓着',
-'抓著書' => '抓著书',
'抓著作' => '抓著作',
'抓著名' => '抓著名',
'抓著錄' => '抓著录',
@@ -12784,7 +13654,6 @@ $zh2Hans = array(
'披著者' => '披著者',
'披著述' => '披著述',
'抬著' => '抬着',
-'抬著書' => '抬著书',
'抬著作' => '抬著作',
'抬著名' => '抬著名',
'抬著錄' => '抬著录',
@@ -12792,7 +13661,6 @@ $zh2Hans = array(
'抬著者' => '抬著者',
'抬著述' => '抬著述',
'抱著' => '抱着',
-'抱著書' => '抱著书',
'抱著作' => '抱著作',
'抱著名' => '抱著名',
'抱著錄' => '抱著录',
@@ -12809,7 +13677,6 @@ $zh2Hans = array(
'拉著述' => '拉著述',
'拉鍊' => '拉链',
'拎著' => '拎着',
-'拎著書' => '拎著书',
'拎著作' => '拎著作',
'拎著名' => '拎著名',
'拎著錄' => '拎著录',
@@ -12817,7 +13684,6 @@ $zh2Hans = array(
'拎著者' => '拎著者',
'拎著述' => '拎著述',
'拖著' => '拖着',
-'拖著書' => '拖著书',
'拖著作' => '拖著作',
'拖著名' => '拖著名',
'拖著錄' => '拖著录',
@@ -12829,7 +13695,6 @@ $zh2Hans = array(
'拚搏' => '拚搏',
'拚死' => '拚死',
'拼著' => '拼着',
-'拼著書' => '拼著书',
'拼著作' => '拼著作',
'拼著名' => '拼著名',
'拼著錄' => '拼著录',
@@ -12837,7 +13702,6 @@ $zh2Hans = array(
'拼著者' => '拼著者',
'拼著述' => '拼著述',
'拿著' => '拿着',
-'拿著書' => '拿著书',
'拿著作' => '拿著作',
'拿著名' => '拿著名',
'拿著錄' => '拿著录',
@@ -12845,7 +13709,6 @@ $zh2Hans = array(
'拿著者' => '拿著者',
'拿著述' => '拿著述',
'持著' => '持着',
-'持著書' => '持著书',
'持著作' => '持著作',
'持著名' => '持著名',
'持著錄' => '持著录',
@@ -12853,7 +13716,6 @@ $zh2Hans = array(
'持著者' => '持著者',
'持著述' => '持著述',
'挑著' => '挑着',
-'挑著書' => '挑著书',
'挑著作' => '挑著作',
'挑著名' => '挑著名',
'挑著錄' => '挑著录',
@@ -12861,7 +13723,6 @@ $zh2Hans = array(
'挑著者' => '挑著者',
'挑著述' => '挑著述',
'擋著' => '挡着',
-'擋著書' => '挡著书',
'擋著作' => '挡著作',
'擋著名' => '挡著名',
'擋著錄' => '挡著录',
@@ -12877,7 +13738,6 @@ $zh2Hans = array(
'掙著者' => '挣著者',
'掙著述' => '挣著述',
'揮著' => '挥着',
-'揮著書' => '挥著书',
'揮著作' => '挥著作',
'揮著名' => '挥著名',
'揮著錄' => '挥著录',
@@ -12885,7 +13745,6 @@ $zh2Hans = array(
'揮著者' => '挥著者',
'揮著述' => '挥著述',
'挨著' => '挨着',
-'挨著書' => '挨著书',
'挨著作' => '挨著作',
'挨著名' => '挨著名',
'挨著錄' => '挨著录',
@@ -12893,7 +13752,6 @@ $zh2Hans = array(
'挨著者' => '挨著者',
'挨著述' => '挨著述',
'捆著' => '捆着',
-'捆著書' => '捆著书',
'捆著作' => '捆著作',
'捆著名' => '捆著名',
'捆著錄' => '捆著录',
@@ -12909,7 +13767,6 @@ $zh2Hans = array(
'據著者' => '据著者',
'據著述' => '据著述',
'掖著' => '掖着',
-'掖著書' => '掖著书',
'掖著作' => '掖著作',
'掖著名' => '掖著名',
'掖著錄' => '掖著录',
@@ -12917,7 +13774,6 @@ $zh2Hans = array(
'掖著者' => '掖著者',
'掖著述' => '掖著述',
'接著' => '接着',
-'接著書' => '接著书',
'接著作' => '接著作',
'接著名' => '接著名',
'接著錄' => '接著录',
@@ -12933,7 +13789,6 @@ $zh2Hans = array(
'揉著者' => '揉著者',
'揉著述' => '揉著述',
'提著' => '提着',
-'提著書' => '提著书',
'提著作' => '提著作',
'提著名' => '提著名',
'提著錄' => '提著录',
@@ -12941,7 +13796,6 @@ $zh2Hans = array(
'提著者' => '提著者',
'提著述' => '提著述',
'摟著' => '搂着',
-'摟著書' => '搂著书',
'摟著作' => '搂著作',
'摟著名' => '搂著名',
'摟著錄' => '搂著录',
@@ -12949,14 +13803,12 @@ $zh2Hans = array(
'摟著者' => '搂著者',
'摟著述' => '搂著述',
'擺著' => '摆着',
-'擺著書' => '摆著书',
'擺著作' => '摆著作',
'擺著名' => '摆著名',
'擺著錄' => '摆著录',
'擺著稱' => '摆著称',
'擺著者' => '摆著者',
'擺著述' => '摆著述',
-'摺疊' => '摺叠',
'撰著' => '撰著',
'撼著' => '撼着',
'撼著書' => '撼著书',
@@ -12966,9 +13818,7 @@ $zh2Hans = array(
'撼著稱' => '撼著称',
'撼著者' => '撼著者',
'撼著述' => '撼著述',
-'效果顯著' => '效果显著',
'敞著' => '敞着',
-'敞著書' => '敞著书',
'敞著作' => '敞著作',
'敞著名' => '敞著名',
'敞著錄' => '敞著录',
@@ -12976,7 +13826,6 @@ $zh2Hans = array(
'敞著者' => '敞著者',
'敞著述' => '敞著述',
'數著' => '数着',
-'數著書' => '数著书',
'數著作' => '数著作',
'數著名' => '数著名',
'數著錄' => '数著录',
@@ -13001,11 +13850,19 @@ $zh2Hans = array(
'斥著述' => '斥著述',
'新著' => '新著',
'新著龍虎門' => '新著龙虎门',
+'於世成' => '於世成',
'於乎' => '於乎',
+'於乙于同' => '於乙于同',
+'於乙宇同' => '於乙宇同',
+'於于同' => '於于同',
+'於哲' => '於哲',
'於夫罗' => '於夫罗',
'於夫羅' => '於夫罗',
'於姓' => '於姓',
+'於宇同' => '於宇同',
'於崇文' => '於崇文',
+'於志賀' => '於志贺',
+'於志贺' => '於志贺',
'於戲' => '於戏',
'於梨華' => '於梨华',
'於梨华' => '於梨华',
@@ -13015,6 +13872,7 @@ $zh2Hans = array(
'於祥玉' => '於祥玉',
'於菟' => '於菟',
'於賢德' => '於贤德',
+'於除鞬' => '於除鞬',
'旋乾轉坤' => '旋乾转坤',
'曠若發矇' => '旷若发矇',
'昂著' => '昂着',
@@ -13040,14 +13898,8 @@ $zh2Hans = array(
'映著述' => '映著述',
'昭著' => '昭著',
'顯著' => '显著',
-'顯著地' => '显著地',
-'顯著地位' => '显著地位',
-'顯著性' => '显著性',
-'顯著成績' => '显著成绩',
-'顯著效果' => '显著效果',
-'顯著特點' => '显著特点',
+'显著' => '显著',
'晃著' => '晃着',
-'晃著書' => '晃著书',
'晃著作' => '晃著作',
'晃著名' => '晃著名',
'晃著錄' => '晃著录',
@@ -13071,7 +13923,6 @@ $zh2Hans = array(
'有著者' => '有著者',
'有著述' => '有著述',
'望著' => '望着',
-'望著書' => '望著书',
'望著作' => '望著作',
'望著名' => '望著名',
'望著錄' => '望著录',
@@ -13080,7 +13931,6 @@ $zh2Hans = array(
'望著述' => '望著述',
'朝乾夕惕' => '朝乾夕惕',
'朝著' => '朝着',
-'朝著書' => '朝著书',
'朝著作' => '朝著作',
'朝著名' => '朝著名',
'朝著錄' => '朝著录',
@@ -13095,6 +13945,7 @@ $zh2Hans = array(
'本著稱' => '本著称',
'本著者' => '本著者',
'本著述' => '本著述',
+'朴於宇同' => '朴於宇同',
'殺著' => '杀着',
'殺著書' => '杀著书',
'殺著作' => '杀著作',
@@ -13112,6 +13963,8 @@ $zh2Hans = array(
'雜著者' => '杂著者',
'雜著述' => '杂著述',
'李乾德' => '李乾德',
+'李乾順' => '李乾顺',
+'李乾顺' => '李乾顺',
'李澤鉅' => '李泽钜',
'來著' => '来着',
'來著書' => '来著书',
@@ -13123,7 +13976,6 @@ $zh2Hans = array(
'來著述' => '来著述',
'楊幺' => '杨幺',
'枕著' => '枕着',
-'枕著書' => '枕著书',
'枕著作' => '枕著作',
'枕著名' => '枕著名',
'枕著錄' => '枕著录',
@@ -13132,6 +13984,8 @@ $zh2Hans = array(
'枕著述' => '枕著述',
'柳詒徵' => '柳诒徵',
'柳诒徵' => '柳诒徵',
+'標志著' => '标志着',
+'標誌著' => '标志着',
'夢著' => '梦着',
'夢著書' => '梦著书',
'夢著作' => '梦著作',
@@ -13141,7 +13995,6 @@ $zh2Hans = array(
'夢著者' => '梦著者',
'夢著述' => '梦著述',
'梳著' => '梳着',
-'梳著書' => '梳著书',
'梳著作' => '梳著作',
'梳著名' => '梳著名',
'梳著錄' => '梳著录',
@@ -13149,7 +14002,6 @@ $zh2Hans = array(
'梳著者' => '梳著者',
'梳著述' => '梳著述',
'樊於期' => '樊於期',
-'比較顯著' => '比较显著',
'氆氌' => '氆氌',
'求著' => '求着',
'求著書' => '求著书',
@@ -13179,6 +14031,7 @@ $zh2Hans = array(
'沿著稱' => '沿著称',
'沿著者' => '沿著者',
'沿著述' => '沿著述',
+'氾濫' => '泛滥',
'洗鍊' => '洗练',
'活著' => '活着',
'活著書' => '活著书',
@@ -13196,6 +14049,7 @@ $zh2Hans = array(
'流著稱' => '流著称',
'流著者' => '流著者',
'流著述' => '流著述',
+'流露著' => '流露着',
'浮著' => '浮着',
'浮著書' => '浮著书',
'浮著作' => '浮著作',
@@ -13274,6 +14128,7 @@ $zh2Hans = array(
'照著稱' => '照著称',
'照著者' => '照著者',
'照著述' => '照著述',
+'愛護著' => '爱护着',
'愛著' => '爱着',
'愛著書' => '爱著书',
'愛著作' => '爱著作',
@@ -13291,6 +14146,7 @@ $zh2Hans = array(
'牽著者' => '牵著者',
'牽著述' => '牵著述',
'犯不著' => '犯不着',
+'犯得著' => '犯得着',
'獨著' => '独着',
'獨著書' => '独著书',
'獨著作' => '独著作',
@@ -13307,6 +14163,7 @@ $zh2Hans = array(
'猜著稱' => '猜著称',
'猜著者' => '猜著者',
'猜著述' => '猜著述',
+'玩著' => '玩着',
'甜著' => '甜着',
'甜著書' => '甜著书',
'甜著作' => '甜著作',
@@ -13316,13 +14173,7 @@ $zh2Hans = array(
'甜著者' => '甜著者',
'甜著述' => '甜著述',
'用不著' => '用不着',
-'用不著書' => '用不着书',
-'用不著作' => '用不著作',
-'用不著名' => '用不著名',
-'用不著錄' => '用不著录',
-'用不著稱' => '用不著称',
-'用不著者' => '用不著者',
-'用不著述' => '用不著述',
+'用得著' => '用得着',
'用著' => '用着',
'用著書' => '用著书',
'用著作' => '用著作',
@@ -13347,7 +14198,6 @@ $zh2Hans = array(
'疑著稱' => '疑著称',
'疑著者' => '疑著者',
'疑著述' => '疑著述',
-'療效顯著' => '疗效显著',
'癥瘕' => '癥瘕',
'皺著' => '皱着',
'皺著書' => '皱著书',
@@ -13381,6 +14231,8 @@ $zh2Hans = array(
'盾著稱' => '盾著称',
'盾著者' => '盾著者',
'盾著述' => '盾著述',
+'看不著' => '看不着',
+'看得著' => '看得着',
'看著' => '看着',
'看著書' => '看着书',
'看著作' => '看著作',
@@ -13492,13 +14344,7 @@ $zh2Hans = array(
'著題' => '着题',
'著魔' => '着魔',
'睡不著' => '睡不着',
-'睡不著書' => '睡不著书',
-'睡不著作' => '睡不著作',
-'睡不著名' => '睡不著名',
-'睡不著錄' => '睡不著录',
-'睡不著稱' => '睡不著称',
-'睡不著者' => '睡不著者',
-'睡不著述' => '睡不著述',
+'睡得著' => '睡得着',
'睡著' => '睡着',
'睡著書' => '睡著书',
'睡著作' => '睡著作',
@@ -13517,6 +14363,14 @@ $zh2Hans = array(
'瞞著稱' => '瞒著称',
'瞞著者' => '瞒著者',
'瞞著述' => '瞒著述',
+'瞧著' => '瞧着',
+'瞧著書' => '瞧着书',
+'瞧著作' => '瞧著作',
+'瞧著名' => '瞧著名',
+'瞧著錄' => '瞧著录',
+'瞧著稱' => '瞧著称',
+'瞧著者' => '瞧著者',
+'瞧著述' => '瞧著述',
'瞪著' => '瞪着',
'瞪著書' => '瞪著书',
'瞪著作' => '瞪著作',
@@ -13644,6 +14498,7 @@ $zh2Hans = array(
'考著稱' => '考著称',
'考著者' => '考著者',
'考著述' => '考著述',
+'肉乾乾' => '肉干干',
'肘手鍊足' => '肘手链足',
'背著' => '背着',
'背著書' => '背著书',
@@ -13677,6 +14532,8 @@ $zh2Hans = array(
'苦著稱' => '苦著称',
'苦著者' => '苦著者',
'苦著述' => '苦著述',
+'苧烯' => '苧烯',
+'薴烯' => '苧烯',
'獲著' => '获着',
'獲著書' => '获著书',
'獲著作' => '获著作',
@@ -13938,6 +14795,8 @@ $zh2Hans = array(
'達著稱' => '达著称',
'達著者' => '达著者',
'達著述' => '达著述',
+'近角聪信' => '近角聪信',
+'近角聰信' => '近角聪信',
'遠著' => '远着',
'遠著書' => '远著书',
'遠著作' => '远著作',
@@ -13954,6 +14813,7 @@ $zh2Hans = array(
'連著稱' => '连著称',
'連著者' => '连著者',
'連著述' => '连著述',
+'迫著' => '迫着',
'追著' => '追着',
'追著書' => '追著书',
'追著作' => '追著作',
@@ -14005,6 +14865,14 @@ $zh2Hans = array(
'釀著稱' => '酿著称',
'釀著者' => '酿著者',
'釀著述' => '酿著述',
+'醯壺' => '醯壶',
+'醯壶' => '醯壶',
+'醯酱' => '醯酱',
+'醯醬' => '醯酱',
+'醯醋' => '醯醋',
+'醯醢' => '醯醢',
+'醯鸡' => '醯鸡',
+'醯雞' => '醯鸡',
'重覆' => '重复',
'金鍊' => '金链',
'鐵鍊' => '铁链',
@@ -14025,6 +14893,7 @@ $zh2Hans = array(
'鎖鍊' => '锁链',
'鍾鍛' => '锺锻',
'鍛鍾' => '锻锺',
+'閻懷禮' => '闫怀礼',
'閉著' => '闭着',
'閉著書' => '闭著书',
'閉著作' => '闭著作',
@@ -14041,6 +14910,10 @@ $zh2Hans = array(
'閑著稱' => '闲著称',
'閑著者' => '闲著者',
'閑著述' => '闲著述',
+'聞不著' => '闻不着',
+'聞得著' => '闻得着',
+'聞著' => '闻着',
+'阿部正瞭' => '阿部正瞭',
'附著' => '附着',
'附睪' => '附睾',
'附著書' => '附著书',
@@ -14094,6 +14967,13 @@ $zh2Hans = array(
'雅著者' => '雅著者',
'雅著述' => '雅著述',
'雍乾' => '雍乾',
+'靠著' => '靠着',
+'靠著作' => '靠著作',
+'靠著名' => '靠著名',
+'靠著錄' => '靠著录',
+'靠著稱' => '靠著称',
+'靠著者' => '靠著者',
+'靠著述' => '靠著述',
'頂著' => '顶着',
'頂著書' => '顶著书',
'頂著作' => '顶著作',
@@ -14128,7 +15008,6 @@ $zh2Hans = array(
'飄著者' => '飘著者',
'飄著述' => '飘著述',
'飭令' => '飭令',
-'餘年' => '馀年',
'駕著' => '驾着',
'駕著書' => '驾著书',
'駕著作' => '驾著作',
@@ -14180,6 +15059,7 @@ $zh2Hans = array(
'鬱姓' => '鬱姓',
'鬱氏' => '鬱氏',
'魏徵' => '魏徵',
+'魚乾乾' => '鱼干干',
'鯰魚' => '鲶鱼',
'麯崇裕' => '麯崇裕',
'麴義' => '麴义',
@@ -14205,6 +15085,8 @@ $zh2TW = array(
'’' => '』',
'三極管' => '三極體',
'三极管' => '三極體',
+'世界裏' => '世界裡',
+'中文裏' => '中文裡',
'串行' => '串列',
'串列加速器' => '串列加速器',
'以太网' => '乙太網',
@@ -14215,9 +15097,12 @@ $zh2TW = array(
'阿塞拜疆' => '亞塞拜然',
'人工智能' => '人工智慧',
'接口' => '介面',
+'任意球員' => '任意球員',
+'任意球员' => '任意球員',
'服务器' => '伺服器',
'字節' => '位元組',
'字节' => '位元組',
+'作品裏' => '作品裡',
'优先级' => '優先順序',
'元兇' => '元凶',
'元凶' => '元凶',
@@ -14226,7 +15111,8 @@ $zh2TW = array(
'克羅地亞' => '克羅埃西亞',
'克罗地亚' => '克羅埃西亞',
'全角' => '全形',
-'雪糕' => '冰淇淋',
+'冬天裏' => '冬天裡',
+'冬日裏' => '冬日裡',
'凉菜' => '冷盤',
'冷菜' => '冷盤',
'凶器' => '凶器',
@@ -14275,6 +15161,7 @@ $zh2TW = array(
'格鲁吉亚' => '喬治亞',
'佐治亚' => '喬治亞',
'佐治亞' => '喬治亞',
+'嘴裏' => '嘴裡',
'土库曼斯坦' => '土庫曼',
'薯仔' => '土豆',
'土豆網' => '土豆網',
@@ -14286,6 +15173,8 @@ $zh2TW = array(
'塞舌尔' => '塞席爾',
'塞舌爾' => '塞席爾',
'塞浦路斯' => '塞普勒斯',
+'夏天裏' => '夏天裡',
+'夏日裏' => '夏日裡',
'多明尼加共和國' => '多明尼加',
'多米尼加共和国' => '多明尼加',
'多米尼加共和國' => '多明尼加',
@@ -14300,19 +15189,23 @@ $zh2TW = array(
'字库' => '字型檔',
'字符集' => '字符集',
'存盘' => '存檔',
+'學裏' => '學裡',
'安提瓜和巴布達' => '安地卡及巴布達',
'安提瓜和巴布达' => '安地卡及巴布達',
'宋元' => '宋元',
'洪都拉斯' => '宏都拉斯',
'寻址' => '定址',
+'寒假裏' => '寒假裡',
'宽带' => '寬頻',
'老撾' => '寮國',
'老挝' => '寮國',
'打门' => '射門',
+'專輯裏' => '專輯裡',
'贊比亞' => '尚比亞',
'赞比亚' => '尚比亞',
'尼日爾' => '尼日',
'尼日尔' => '尼日',
+'山洞裏' => '山洞裡',
'巴布亞新畿內亞' => '巴布亞紐幾內亞',
'巴布亚新几内亚' => '巴布亞紐幾內亞',
'巴巴多斯' => '巴貝多',
@@ -14324,6 +15217,7 @@ $zh2TW = array(
'例程' => '常式',
'平治之乱' => '平治之亂',
'平治之亂' => '平治之亂',
+'年代裏' => '年代裡',
'几内亚比绍' => '幾內亞比索',
'幾內亞比紹' => '幾內亞比索',
'彩带' => '彩帶',
@@ -14332,11 +15226,13 @@ $zh2TW = array(
'彩牌楼' => '彩牌樓',
'復蘇' => '復甦',
'复苏' => '復甦',
+'心裏' => '心裡',
'快闪存储器' => '快閃記憶體',
'闪存' => '快閃記憶體',
'传感' => '感測',
'习用' => '慣用',
'戏彩娱亲' => '戲綵娛親',
+'戲裏' => '戲裡',
'手电筒' => '手電筒',
'手电' => '手電筒',
'括号' => '括弧',
@@ -14344,23 +15240,35 @@ $zh2TW = array(
'拿破仑' => '拿破崙',
'積架' => '捷豹',
'扫瞄仪' => '掃瞄器',
+'挂钩' => '掛鉤',
+'掛鈎' => '掛鉤',
'控件' => '控制項',
'台球' => '撞球',
'桌球' => '撞球',
'便携式' => '攜帶型',
+'故事裏' => '故事裡',
'调制解调器' => '數據機',
'調制解調器' => '數據機',
'斯洛文尼亞' => '斯洛維尼亞',
'斯洛文尼亚' => '斯洛維尼亞',
'新纪元' => '新紀元',
'新紀元' => '新紀元',
+'日子裏' => '日子裡',
+'春假裏' => '春假裡',
+'春天裏' => '春天裡',
+'春日裏' => '春日裡',
+'時間裏' => '時間裡',
'芯片' => '晶元',
+'暑假裏' => '暑假裡',
+'村子裏' => '村子裡',
'乍得' => '查德',
'克林頓' => '柯林頓',
'克林顿' => '柯林頓',
'格林納達' => '格瑞那達',
'格林纳达' => '格瑞那達',
'凡高' => '梵谷',
+'森林裏' => '森林裡',
+'棺材裏' => '棺材裡',
'榴蓮' => '榴槤',
'榴莲' => '榴槤',
'仿真' => '模擬',
@@ -14369,6 +15277,7 @@ $zh2TW = array(
'機械人' => '機器人',
'机器人' => '機器人',
'字段' => '欄位',
+'歷史裏' => '歷史裡',
'元音' => '母音',
'永历' => '永曆',
'文莱' => '汶萊',
@@ -14380,11 +15289,13 @@ $zh2TW = array(
'博茨瓦納' => '波札那',
'侯赛因' => '海珊',
'侯賽因' => '海珊',
+'深淵裏' => '深淵裡',
'光标' => '游標',
'鼠标' => '滑鼠',
'算法' => '演算法',
'乌兹别克斯坦' => '烏茲別克',
'词组' => '片語',
+'獄裏' => '獄裡',
'塞拉利昂' => '獅子山',
'危地马拉' => '瓜地馬拉',
'危地馬拉' => '瓜地馬拉',
@@ -14392,10 +15303,13 @@ $zh2TW = array(
'岡比亞' => '甘比亞',
'疑兇' => '疑凶',
'疑凶' => '疑凶',
+'百科裏' => '百科裡',
+'皮裏陽秋' => '皮裡陽秋',
'盧旺達' => '盧安達',
'卢旺达' => '盧安達',
'真凶' => '真凶',
'真兇' => '真凶',
+'眼睛裏' => '眼睛裡',
'硅片' => '矽片',
'硅谷' => '矽谷',
'硬盘' => '硬碟',
@@ -14404,6 +15318,9 @@ $zh2TW = array(
'磁盘' => '磁碟',
'磁道' => '磁軌',
'福士' => '福斯',
+'秋假裏' => '秋假裡',
+'秋天裏' => '秋天裡',
+'秋日裏' => '秋日裡',
'程控' => '程式控制',
'突尼斯' => '突尼西亞',
'尾注' => '章節附註',
@@ -14412,6 +15329,7 @@ $zh2TW = array(
'等于' => '等於',
'短訊' => '簡訊',
'短信' => '簡訊',
+'系列裏' => '系列裡',
'新西蘭' => '紐西蘭',
'新西兰' => '紐西蘭',
'所罗门群岛' => '索羅門群島',
@@ -14442,18 +15360,20 @@ $zh2TW = array(
'聖盧西亞' => '聖露西亞',
'圣马力诺' => '聖馬利諾',
'聖馬力諾' => '聖馬利諾',
+'肚裏' => '肚裡',
'肯尼亚' => '肯亞',
'肯雅' => '肯亞',
'任意球' => '自由球',
'航天大学' => '航天大學',
+'苦裏' => '苦裡',
'毛里塔尼亚' => '茅利塔尼亞',
'毛里塔尼亞' => '茅利塔尼亞',
'莫桑比克' => '莫三比克',
'万历' => '萬曆',
'瓦努阿图' => '萬那杜',
'瓦努阿圖' => '萬那杜',
-'也门' => '葉門',
'也門' => '葉門',
+'也门' => '葉門',
'着' => '著',
'科摩羅' => '葛摩',
'科摩罗' => '葛摩',
@@ -14470,10 +15390,13 @@ $zh2TW = array(
'流動電話' => '行動電話',
'移动电话' => '行動電話',
'行程控制' => '行程控制',
+'衞' => '衛',
'卫生' => '衛生',
'衞生' => '衛生',
'埃塞俄比亚' => '衣索比亞',
'埃塞俄比亞' => '衣索比亞',
+'裏勾外連' => '裡勾外連',
+'裏面' => '裡面',
'分辨率' => '解析度',
'译码' => '解碼',
'出租车' => '計程車',
@@ -14508,6 +15431,7 @@ $zh2TW = array(
'加納' => '迦納',
'追凶' => '追凶',
'追兇' => '追凶',
+'這裏' => '這裡',
'信道' => '通道',
'逞凶鬥狠' => '逞凶鬥狠',
'逞兇鬥狠' => '逞凶鬥狠',
@@ -14522,20 +15446,30 @@ $zh2TW = array(
'遠程控制' => '遠程控制',
'远程控制' => '遠程控制',
'溫納圖萬' => '那杜',
+'醫院裏' => '醫院裡',
+'酰' => '醯',
'巨商' => '鉅賈',
+'钩' => '鉤',
+'鈎' => '鉤',
+'钩心斗角' => '鉤心鬥角',
+'鈎心鬥角' => '鉤心鬥角',
'写保护' => '防寫',
'阿拉伯联合酋长国' => '阿拉伯聯合大公國',
'阿拉伯聯合酋長國' => '阿拉伯聯合大公國',
'噪声' => '雜訊',
'脱机' => '離線',
+'雪裏紅' => '雪裡紅',
+'雪裏蕻' => '雪裡蕻',
'雪铁龙' => '雪鐵龍',
'异步' => '非同步',
'声卡' => '音效卡',
'缺省' => '預設',
'颁布' => '頒布',
'頒佈' => '頒布',
+'領域裏' => '領域裡',
'头球' => '頭槌',
'粒入球' => '顆進球',
+'館裏' => '館裡',
'马里共和国' => '馬利共和國',
'馬里共和國' => '馬利共和國',
'马耳他' => '馬爾他',
@@ -14544,6 +15478,7 @@ $zh2TW = array(
'萬事得' => '馬自達',
'狄安娜' => '黛安娜',
'戴安娜' => '黛安娜',
+'點裏' => '點裡',
'位图' => '點陣圖',
);
@@ -14555,6 +15490,10 @@ $zh2HK = array(
'三極體' => '三極管',
'不著痕跡' => '不着痕跡',
'不著邊際' => '不着邊際',
+'世界裡' => '世界裏',
+'世界里' => '世界裏',
+'中文里' => '中文裏',
+'中文裡' => '中文裏',
'民乐' => '中樂',
'华乐' => '中樂',
'查德' => '乍得',
@@ -14623,6 +15562,8 @@ $zh2HK = array(
'住著述' => '住著述',
'住著錄' => '住著錄',
'維德角' => '佛得角',
+'作品裡' => '作品裏',
+'作品里' => '作品裏',
'來著' => '來着',
'來著作' => '來著作',
'來著名' => '來著名',
@@ -14727,6 +15668,13 @@ $zh2HK = array(
'冒著者' => '冒著者',
'冒著述' => '冒著述',
'冒著錄' => '冒著錄',
+'冬天里' => '冬天裏',
+'冬天裡' => '冬天裏',
+'冬日裡' => '冬日裏',
+'冬日里' => '冬日裏',
+'分布' => '分佈',
+'分布於' => '分佈於',
+'分布于' => '分佈於',
'列支敦斯登' => '列支敦士登',
'賴比瑞亞' => '利比里亞',
'制著' => '制着',
@@ -14771,6 +15719,7 @@ $zh2HK = array(
'動著者' => '動著者',
'動著述' => '動著述',
'動著錄' => '動著錄',
+'医院里' => '医院裏',
'波札那' => '博茨瓦納',
'珍妮弗·卡普里亚蒂' => '卡佩雅蒂',
'印著' => '印着',
@@ -14814,6 +15763,11 @@ $zh2HK = array(
'叫著者' => '叫著者',
'叫著述' => '叫著述',
'叫著錄' => '叫著錄',
+'叱吒' => '叱咤',
+'叱咤' => '叱咤',
+'吃不著' => '吃不着',
+'吃得著' => '吃得着',
+'吃著' => '吃着',
'吉布地' => '吉布堤',
'向著' => '向着',
'向著作' => '向著作',
@@ -14847,6 +15801,7 @@ $zh2HK = array(
'味著者' => '味著者',
'味著述' => '味著述',
'味著錄' => '味著錄',
+'咤' => '咤',
'哥斯大黎加' => '哥斯達黎加',
'哭著' => '哭着',
'哭著作' => '哭著作',
@@ -14873,6 +15828,11 @@ $zh2HK = array(
'喝著述' => '喝著述',
'喝著錄' => '喝著錄',
'自行车' => '單車',
+'嗅不著' => '嗅不着',
+'嗅得著' => '嗅得着',
+'嗅著' => '嗅着',
+'嘴里' => '嘴裏',
+'嘴裡' => '嘴裏',
'嚷著' => '嚷着',
'嚷著作' => '嚷著作',
'嚷著名' => '嚷著名',
@@ -14939,6 +15899,10 @@ $zh2HK = array(
'壓著者' => '壓著者',
'壓著述' => '壓著述',
'壓著錄' => '壓著錄',
+'夏天里' => '夏天裏',
+'夏天裡' => '夏天裏',
+'夏日里' => '夏日裏',
+'夏日裡' => '夏日裏',
'夢著' => '夢着',
'夢著作' => '夢著作',
'夢著名' => '夢著名',
@@ -14972,6 +15936,8 @@ $zh2HK = array(
'學著者' => '學著者',
'學著述' => '學著述',
'學著錄' => '學著錄',
+'學裡' => '學裏',
+'学里' => '學裏',
'守著' => '守着',
'守著作' => '守著作',
'守著名' => '守著名',
@@ -14990,6 +15956,8 @@ $zh2HK = array(
'定著述' => '定著述',
'定著錄' => '定著錄',
'沃尓沃' => '富豪',
+'寒假裡' => '寒假裏',
+'寒假里' => '寒假裏',
'寫著' => '寫着',
'寫著作' => '寫著作',
'寫著名' => '寫著名',
@@ -14998,6 +15966,8 @@ $zh2HK = array(
'寫著者' => '寫著者',
'寫著述' => '寫著述',
'寫著錄' => '寫著錄',
+'专辑里' => '專輯裏',
+'專輯裡' => '專輯裏',
'尋著' => '尋着',
'尋著作' => '尋著作',
'尋著名' => '尋著名',
@@ -15028,11 +15998,15 @@ $zh2HK = array(
'展著者' => '展著者',
'展著述' => '展著述',
'展著錄' => '展著錄',
+'山洞裡' => '山洞裏',
+'山洞里' => '山洞裏',
'甘比亞' => '岡比亞',
'公車' => '巴士',
'巴貝多' => '巴巴多斯',
'巴布亞紐幾內亞' => '巴布亞新畿內亞',
'布吉納法索' => '布基納法索',
+'布希亞' => '布希亞',
+'布希亚' => '布希亞',
'布希' => '布殊',
'布什' => '布殊',
'蒲隆地' => '布隆迪',
@@ -15054,7 +16028,12 @@ $zh2HK = array(
'幫著者' => '幫著者',
'幫著述' => '幫著述',
'幫著錄' => '幫著錄',
+'干着急' => '干着急',
'賓士' => '平治',
+'年代里' => '年代裏',
+'年代裡' => '年代裏',
+'幹著' => '幹着',
+'干着' => '幹着',
'幾內亞比索' => '幾內亞比紹',
'康著' => '康着',
'康著作' => '康著作',
@@ -15089,6 +16068,7 @@ $zh2HK = array(
'循著述' => '循著述',
'循著錄' => '循著錄',
'心著' => '心着',
+'心繫著' => '心繫着',
'心著作' => '心著作',
'心著名' => '心著名',
'心著書' => '心著書',
@@ -15096,6 +16076,8 @@ $zh2HK = array(
'心著者' => '心著者',
'心著述' => '心著述',
'心著錄' => '心著錄',
+'心裡' => '心裏',
+'心里' => '心裏',
'忍著' => '忍着',
'忍著作' => '忍著作',
'忍著名' => '忍著名',
@@ -15201,6 +16183,8 @@ $zh2HK = array(
'戰著者' => '戰著者',
'戰著述' => '戰著述',
'戰著錄' => '戰著錄',
+'戲裡' => '戲裏',
+'戏里' => '戲裏',
'黛安娜' => '戴安娜',
'狄安娜' => '戴安娜',
'戴著' => '戴着',
@@ -15231,17 +16215,10 @@ $zh2HK = array(
'扛著述' => '扛著述',
'扛著錄' => '扛著錄',
'找不著' => '找不着',
-'找不著作' => '找不著作',
-'找不著名' => '找不著名',
-'找不著書' => '找不著書',
-'找不著稱' => '找不著稱',
-'找不著者' => '找不著者',
-'找不著述' => '找不著述',
-'找不著錄' => '找不著錄',
+'找得著' => '找得着',
'抓著' => '抓着',
'抓著作' => '抓著作',
'抓著名' => '抓著名',
-'抓著書' => '抓著書',
'抓著稱' => '抓著稱',
'抓著者' => '抓著者',
'抓著述' => '抓著述',
@@ -15257,7 +16234,6 @@ $zh2HK = array(
'抬著' => '抬着',
'抬著作' => '抬著作',
'抬著名' => '抬著名',
-'抬著書' => '抬著書',
'抬著稱' => '抬著稱',
'抬著者' => '抬著者',
'抬著述' => '抬著述',
@@ -15265,7 +16241,6 @@ $zh2HK = array(
'抱著' => '抱着',
'抱著作' => '抱著作',
'抱著名' => '抱著名',
-'抱著書' => '抱著書',
'抱著稱' => '抱著稱',
'抱著者' => '抱著者',
'抱著述' => '抱著述',
@@ -15281,7 +16256,6 @@ $zh2HK = array(
'拎著' => '拎着',
'拎著作' => '拎著作',
'拎著名' => '拎著名',
-'拎著書' => '拎著書',
'拎著稱' => '拎著稱',
'拎著者' => '拎著者',
'拎著述' => '拎著述',
@@ -15289,7 +16263,6 @@ $zh2HK = array(
'拖著' => '拖着',
'拖著作' => '拖著作',
'拖著名' => '拖著名',
-'拖著書' => '拖著書',
'拖著稱' => '拖著稱',
'拖著者' => '拖著者',
'拖著述' => '拖著述',
@@ -15297,7 +16270,6 @@ $zh2HK = array(
'拼著' => '拼着',
'拼著作' => '拼著作',
'拼著名' => '拼著名',
-'拼著書' => '拼著書',
'拼著稱' => '拼著稱',
'拼著者' => '拼著者',
'拼著述' => '拼著述',
@@ -15306,7 +16278,6 @@ $zh2HK = array(
'拿破崙' => '拿破侖',
'拿著作' => '拿著作',
'拿著名' => '拿著名',
-'拿著書' => '拿著書',
'拿著稱' => '拿著稱',
'拿著者' => '拿著者',
'拿著述' => '拿著述',
@@ -15314,7 +16285,6 @@ $zh2HK = array(
'持著' => '持着',
'持著作' => '持著作',
'持著名' => '持著名',
-'持著書' => '持著書',
'持著稱' => '持著稱',
'持著者' => '持著者',
'持著述' => '持著述',
@@ -15322,7 +16292,6 @@ $zh2HK = array(
'挑著' => '挑着',
'挑著作' => '挑著作',
'挑著名' => '挑著名',
-'挑著書' => '挑著書',
'挑著稱' => '挑著稱',
'挑著者' => '挑著者',
'挑著述' => '挑著述',
@@ -15330,7 +16299,6 @@ $zh2HK = array(
'挨著' => '挨着',
'挨著作' => '挨著作',
'挨著名' => '挨著名',
-'挨著書' => '挨著書',
'挨著稱' => '挨著稱',
'挨著者' => '挨著者',
'挨著述' => '挨著述',
@@ -15338,7 +16306,6 @@ $zh2HK = array(
'捆著' => '捆着',
'捆著作' => '捆著作',
'捆著名' => '捆著名',
-'捆著書' => '捆著書',
'捆著稱' => '捆著稱',
'捆著者' => '捆著者',
'捆著述' => '捆著述',
@@ -15346,7 +16313,6 @@ $zh2HK = array(
'掖著' => '掖着',
'掖著作' => '掖著作',
'掖著名' => '掖著名',
-'掖著書' => '掖著書',
'掖著稱' => '掖著稱',
'掖著者' => '掖著者',
'掖著述' => '掖著述',
@@ -15359,10 +16325,10 @@ $zh2HK = array(
'掙著者' => '掙著者',
'掙著述' => '掙著述',
'掙著錄' => '掙著錄',
+'掛鉤' => '掛鈎',
'接著' => '接着',
'接著作' => '接著作',
'接著名' => '接著名',
-'接著書' => '接著書',
'接著稱' => '接著稱',
'接著者' => '接著者',
'接著述' => '接著述',
@@ -15378,7 +16344,6 @@ $zh2HK = array(
'提著' => '提着',
'提著作' => '提著作',
'提著名' => '提著名',
-'提著書' => '提著書',
'提著稱' => '提著稱',
'提著者' => '提著者',
'提著述' => '提著述',
@@ -15386,7 +16351,6 @@ $zh2HK = array(
'揮著' => '揮着',
'揮著作' => '揮著作',
'揮著名' => '揮著名',
-'揮著書' => '揮著書',
'揮著稱' => '揮著稱',
'揮著者' => '揮著者',
'揮著述' => '揮著述',
@@ -15394,7 +16358,6 @@ $zh2HK = array(
'摟著' => '摟着',
'摟著作' => '摟著作',
'摟著名' => '摟著名',
-'摟著書' => '摟著書',
'摟著稱' => '摟著稱',
'摟著者' => '摟著者',
'摟著述' => '摟著述',
@@ -15410,7 +16373,6 @@ $zh2HK = array(
'擋著' => '擋着',
'擋著作' => '擋著作',
'擋著名' => '擋著名',
-'擋著書' => '擋著書',
'擋著稱' => '擋著稱',
'擋著者' => '擋著者',
'擋著述' => '擋著述',
@@ -15426,15 +16388,15 @@ $zh2HK = array(
'擺著' => '擺着',
'擺著作' => '擺著作',
'擺著名' => '擺著名',
-'擺著書' => '擺著書',
'擺著稱' => '擺著稱',
'擺著者' => '擺著者',
'擺著述' => '擺著述',
'擺著錄' => '擺著錄',
+'故事里' => '故事裏',
+'故事裡' => '故事裏',
'敞著' => '敞着',
'敞著作' => '敞著作',
'敞著名' => '敞著名',
-'敞著書' => '敞著書',
'敞著稱' => '敞著稱',
'敞著者' => '敞著者',
'敞著述' => '敞著述',
@@ -15442,7 +16404,6 @@ $zh2HK = array(
'數著' => '數着',
'數著作' => '數著作',
'數著名' => '數著名',
-'數著書' => '數著書',
'數著稱' => '數著稱',
'數著者' => '數著者',
'數著述' => '數著述',
@@ -15459,6 +16420,8 @@ $zh2HK = array(
'斯洛維尼亞' => '斯洛文尼亞',
'新著龍虎門' => '新著龍虎門',
'紐西蘭' => '新西蘭',
+'日子里' => '日子裏',
+'日子裡' => '日子裏',
'昂著' => '昂着',
'昂著作' => '昂著作',
'昂著名' => '昂著名',
@@ -15475,14 +16438,23 @@ $zh2HK = array(
'映著者' => '映著者',
'映著述' => '映著述',
'映著錄' => '映著錄',
+'春假里' => '春假裏',
+'春假裡' => '春假裏',
+'春天裡' => '春天裏',
+'春天里' => '春天裏',
+'春日裡' => '春日裏',
+'春日里' => '春日裏',
+'时间里' => '時間裏',
+'時間裡' => '時間裏',
'晃著' => '晃着',
'晃著作' => '晃著作',
'晃著名' => '晃著名',
-'晃著書' => '晃著書',
'晃著稱' => '晃著稱',
'晃著者' => '晃著者',
'晃著述' => '晃著述',
'晃著錄' => '晃著錄',
+'暑假里' => '暑假裏',
+'暑假裡' => '暑假裏',
'暗著' => '暗着',
'暗著作' => '暗著作',
'暗著名' => '暗著名',
@@ -15510,7 +16482,6 @@ $zh2HK = array(
'朝著' => '朝着',
'朝著作' => '朝著作',
'朝著名' => '朝著名',
-'朝著書' => '朝著書',
'朝著稱' => '朝著稱',
'朝著者' => '朝著者',
'朝著述' => '朝著述',
@@ -15523,10 +16494,11 @@ $zh2HK = array(
'本著者' => '本著者',
'本著述' => '本著述',
'本著錄' => '本著錄',
+'村子里' => '村子裏',
+'村子裡' => '村子裏',
'枕著' => '枕着',
'枕著作' => '枕著作',
'枕著名' => '枕著名',
-'枕著書' => '枕著書',
'枕著稱' => '枕著稱',
'枕著者' => '枕著者',
'枕著述' => '枕著述',
@@ -15537,11 +16509,14 @@ $zh2HK = array(
'梳著' => '梳着',
'梳著作' => '梳著作',
'梳著名' => '梳著名',
-'梳著書' => '梳著書',
'梳著稱' => '梳著稱',
'梳著者' => '梳著者',
'梳著述' => '梳著述',
'梳著錄' => '梳著錄',
+'森林裡' => '森林裏',
+'森林里' => '森林裏',
+'棺材裡' => '棺材裏',
+'棺材里' => '棺材裏',
'榴蓮' => '榴槤',
'榴莲' => '榴槤',
'樂著' => '樂着',
@@ -15553,8 +16528,11 @@ $zh2HK = array(
'樂著述' => '樂著述',
'樂著錄' => '樂著錄',
'寶獅' => '標致',
+'標誌著' => '標誌着',
'機器人' => '機械人',
'机器人' => '機械人',
+'历史里' => '歷史裏',
+'歷史裡' => '歷史裏',
'殺著' => '殺着',
'殺著作' => '殺著作',
'殺著名' => '殺著名',
@@ -15615,6 +16593,7 @@ $zh2HK = array(
'流著者' => '流著者',
'流著述' => '流著述',
'流著錄' => '流著錄',
+'流露著' => '流露着',
'浮著' => '浮着',
'浮著作' => '浮著作',
'浮著名' => '浮著名',
@@ -15639,6 +16618,8 @@ $zh2HK = array(
'涼著者' => '涼著者',
'涼著述' => '涼著述',
'涼著錄' => '涼著錄',
+'深淵裡' => '深淵裏',
+'深渊里' => '深渊裏',
'渴著' => '渴着',
'渴著作' => '渴著作',
'渴著名' => '渴著名',
@@ -15679,6 +16660,7 @@ $zh2HK = array(
'潤著者' => '潤著者',
'潤著述' => '潤著述',
'潤著錄' => '潤著錄',
+'菸' => '煙',
'照著' => '照着',
'照著作' => '照著作',
'照著名' => '照著名',
@@ -15720,6 +16702,7 @@ $zh2HK = array(
'犯不著者' => '犯不著者',
'犯不著述' => '犯不著述',
'犯不著錄' => '犯不著錄',
+'犯得著' => '犯得着',
'犬只' => '狗隻',
'猜著' => '猜着',
'猜著作' => '猜著作',
@@ -15729,6 +16712,8 @@ $zh2HK = array(
'猜著者' => '猜著者',
'猜著述' => '猜著述',
'猜著錄' => '猜著錄',
+'狱里' => '獄裏',
+'獄裡' => '獄裏',
'獨著' => '獨着',
'獨著作' => '獨著作',
'獨著名' => '獨著名',
@@ -15756,13 +16741,7 @@ $zh2HK = array(
'甜著述' => '甜著述',
'甜著錄' => '甜著錄',
'用不著' => '用不着',
-'用不著作' => '用不著作',
-'用不著名' => '用不著名',
-'用不著書' => '用不著書',
-'用不著稱' => '用不著稱',
-'用不著者' => '用不著者',
-'用不著述' => '用不著述',
-'用不著錄' => '用不著錄',
+'用得著' => '用得着',
'用著' => '用着',
'用著作' => '用著作',
'用著名' => '用著名',
@@ -15797,8 +16776,12 @@ $zh2HK = array(
'疑著錄' => '疑著錄',
'发布' => '發佈',
'發布' => '發佈',
+'百科裡' => '百科裏',
+'百科里' => '百科裏',
'計程車' => '的士',
'出租车' => '的士',
+'皮里阳秋' => '皮裏陽秋',
+'皮裡陽秋' => '皮裏陽秋',
'皺著' => '皺着',
'皺著作' => '皺著作',
'皺著名' => '皺著名',
@@ -15832,6 +16815,8 @@ $zh2HK = array(
'盾著者' => '盾著者',
'盾著述' => '盾著述',
'盾著錄' => '盾著錄',
+'看不著' => '看不着',
+'看得著' => '看得着',
'看著' => '看着',
'看著作' => '看著作',
'看著名' => '看著名',
@@ -15840,6 +16825,8 @@ $zh2HK = array(
'看著者' => '看著者',
'看著述' => '看著述',
'看著錄' => '看著錄',
+'眼睛裡' => '眼睛裏',
+'眼睛里' => '眼睛裏',
'著什麼急' => '着什麼急',
'著他' => '着他',
'著你' => '着你',
@@ -15877,13 +16864,7 @@ $zh2HK = array(
'著陸' => '着陸',
'著鞭' => '着鞭',
'睡不著' => '睡不着',
-'睡不著作' => '睡不著作',
-'睡不著名' => '睡不著名',
-'睡不著書' => '睡不著書',
-'睡不著稱' => '睡不著稱',
-'睡不著者' => '睡不著者',
-'睡不著述' => '睡不著述',
-'睡不著錄' => '睡不著錄',
+'睡得著' => '睡得着',
'睡著' => '睡着',
'睡著作' => '睡著作',
'睡著名' => '睡著名',
@@ -15921,6 +16902,12 @@ $zh2HK = array(
'福著者' => '福著者',
'福著述' => '福著述',
'福著錄' => '福著錄',
+'秋假裡' => '秋假裏',
+'秋假里' => '秋假裏',
+'秋天裡' => '秋天裏',
+'秋天里' => '秋天裏',
+'秋日里' => '秋日裏',
+'秋日裡' => '秋日裏',
'葛摩' => '科摩羅',
'捷豹' => '積架',
'空著' => '空着',
@@ -15966,6 +16953,8 @@ $zh2HK = array(
'管著述' => '管著述',
'管著錄' => '管著錄',
'迈克尔·欧文' => '米高奧雲',
+'系列裡' => '系列裏',
+'系列里' => '系列裏',
'索馬利亞' => '索馬里',
'紮著' => '紮着',
'紮著作' => '紮著作',
@@ -16047,6 +17036,8 @@ $zh2HK = array(
'聖文森及格瑞那丁' => '聖文森特和格林納丁斯',
'聖露西亞' => '聖盧西亞',
'聖馬利諾' => '聖馬力諾',
+'聽不著' => '聽不着',
+'聽得著' => '聽得着',
'聽著' => '聽着',
'聽著作' => '聽著作',
'聽著名' => '聽著名',
@@ -16055,6 +17046,8 @@ $zh2HK = array(
'聽著者' => '聽著者',
'聽著述' => '聽著述',
'聽著錄' => '聽著錄',
+'肚里' => '肚裏',
+'肚裡' => '肚裏',
'肯尼亚' => '肯雅',
'肯亞' => '肯雅',
'背著' => '背着',
@@ -16098,6 +17091,8 @@ $zh2HK = array(
'苦著者' => '苦著者',
'苦著述' => '苦著述',
'苦著錄' => '苦著錄',
+'苦里' => '苦裏',
+'苦裡' => '苦裏',
'莫三比克' => '莫桑比克',
'賴索托' => '萊索托',
'馬自達' => '萬事得',
@@ -16119,6 +17114,7 @@ $zh2HK = array(
'蒙著述' => '蒙著述',
'蒙著錄' => '蒙著錄',
'萨达姆' => '薩達姆',
+'藉著' => '藉着',
'藏著' => '藏着',
'藏著作' => '藏著作',
'藏著名' => '藏著名',
@@ -16151,6 +17147,7 @@ $zh2HK = array(
'行著者' => '行著者',
'行著述' => '行著述',
'行著錄' => '行著錄',
+'衛' => '衞',
'衣著' => '衣着',
'衣著作' => '衣著作',
'衣著名' => '衣著名',
@@ -16159,6 +17156,10 @@ $zh2HK = array(
'衣著者' => '衣著者',
'衣著述' => '衣著述',
'衣著錄' => '衣著錄',
+'裡勾外連' => '裏勾外連',
+'里勾外连' => '裏勾外連',
+'里面' => '裏面',
+'裡面' => '裏面',
'裝著' => '裝着',
'裝著作' => '裝著作',
'裝著名' => '裝著名',
@@ -16302,7 +17303,6 @@ $zh2HK = array(
'踏著' => '踏着',
'踏著作' => '踏著作',
'踏著名' => '踏著名',
-'踏著書' => '踏著書',
'踏著稱' => '踏著稱',
'踏著者' => '踏著者',
'踏著述' => '踏著述',
@@ -16364,6 +17364,9 @@ $zh2HK = array(
'辦著者' => '辦著者',
'辦著述' => '辦著述',
'辦著錄' => '辦著錄',
+'近角聪信' => '近角聰信',
+'近角聰信' => '近角聰信',
+'迫著' => '迫着',
'追著' => '追着',
'追著作' => '追著作',
'追著名' => '追著名',
@@ -16380,6 +17383,8 @@ $zh2HK = array(
'逆著者' => '逆著者',
'逆著述' => '逆著述',
'逆著錄' => '逆著錄',
+'這里' => '這裏',
+'這裡' => '這裏',
'連著' => '連着',
'連著作' => '連著作',
'連著名' => '連著名',
@@ -16428,6 +17433,7 @@ $zh2HK = array(
'配著者' => '配著者',
'配著述' => '配著述',
'配著錄' => '配著錄',
+'醯' => '酰',
'醜著' => '醜着',
'醜著作' => '醜著作',
'醜著名' => '醜著名',
@@ -16436,6 +17442,15 @@ $zh2HK = array(
'醜著者' => '醜著者',
'醜著述' => '醜著述',
'醜著錄' => '醜著錄',
+'醫院裡' => '醫院裏',
+'醯壺' => '醯壺',
+'醯壶' => '醯壺',
+'醯醋' => '醯醋',
+'醯醢' => '醯醢',
+'醯醬' => '醯醬',
+'醯酱' => '醯醬',
+'醯鸡' => '醯雞',
+'醯雞' => '醯雞',
'釀著' => '釀着',
'釀著作' => '釀著作',
'釀著名' => '釀著名',
@@ -16444,6 +17459,8 @@ $zh2HK = array(
'釀著者' => '釀著者',
'釀著述' => '釀著述',
'釀著錄' => '釀著錄',
+'鉤' => '鈎',
+'鉤心鬥角' => '鈎心鬥角',
'鋪著' => '鋪着',
'鋪著作' => '鋪著作',
'鋪著名' => '鋪著名',
@@ -16484,6 +17501,9 @@ $zh2HK = array(
'關著者' => '關著者',
'關著述' => '關著述',
'關著錄' => '關著錄',
+'聞不著' => '闻不着',
+'聞得著' => '闻得着',
+'聞著' => '闻着',
'亞塞拜然' => '阿塞拜疆',
'阿拉伯聯合大公國' => '阿拉伯聯合酋長國',
'附著' => '附着',
@@ -16543,6 +17563,19 @@ $zh2HK = array(
'雜著述' => '雜著述',
'雜著錄' => '雜著錄',
'冰淇淋' => '雪糕',
+'雪里红' => '雪裏紅',
+'雪裡紅' => '雪裏紅',
+'雪裡蕻' => '雪裏蕻',
+'雪里蕻' => '雪裏蕻',
+'靠著' => '靠着',
+'靠著作' => '靠著作',
+'靠著名' => '靠著名',
+'靠著稱' => '靠著稱',
+'靠著称' => '靠著稱',
+'靠著者' => '靠著者',
+'靠著述' => '靠著述',
+'靠著錄' => '靠著錄',
+'靠著录' => '靠著錄',
'響著' => '響着',
'響著作' => '響著作',
'響著名' => '響著名',
@@ -16569,6 +17602,8 @@ $zh2HK = array(
'順著錄' => '順著錄',
'頒布' => '頒佈',
'颁布' => '頒佈',
+'領域裡' => '領域裏',
+'领域里' => '領域裏',
'領著' => '領着',
'領著作' => '領著作',
'領著名' => '領著名',
@@ -16585,6 +17620,8 @@ $zh2HK = array(
'飄著者' => '飄著者',
'飄著述' => '飄著述',
'飄著錄' => '飄著錄',
+'館裡' => '館裏',
+'馆里' => '館裏',
'馬爾地夫' => '馬爾代夫',
'馬利共和國' => '馬里共和國',
'土豆' => '馬鈴薯',
@@ -16660,6 +17697,8 @@ $zh2HK = array(
'點著者' => '點著者',
'點著述' => '點著述',
'點著錄' => '點著錄',
+'點裡' => '點裏',
+'点里' => '點裏',
);
$zh2CN = array(
@@ -16697,6 +17736,7 @@ $zh2CN = array(
'維德角' => '佛得角',
'常式' => '例程',
'侏儸紀' => '侏罗纪',
+'海珊' => '侯赛因',
'攜帶型' => '便携式',
'資訊理論' => '信息论',
'母音' => '元音',
@@ -16731,6 +17771,7 @@ $zh2CN = array(
'十進位制' => '十进位制',
'十進位' => '十进制',
'半形' => '半角',
+'华乐街' => '华乐街',
'波札那' => '博茨瓦纳',
'盧安達' => '卢旺达',
'衞生' => '卫生',
@@ -16775,7 +17816,10 @@ $zh2CN = array(
'賓士' => '奔驰',
'平治' => '奔驰',
'忌廉' => '奶油',
-'乳酪' => '奶酪',
+'字元会' => '字元会',
+'字元會' => '字元会',
+'字元濟' => '字元济',
+'字元济' => '字元济',
'字型大小' => '字号',
'字型檔' => '字库',
'欄位' => '字段',
@@ -16801,6 +17845,8 @@ $zh2CN = array(
'布殊' => '布什',
'布基納法索' => '布基纳法索',
'布吉納法索' => '布基纳法索',
+'布希亞' => '布希亚',
+'布希亚' => '布希亚',
'蒲隆地' => '布隆迪',
'希特拉' => '希特勒',
'帛琉' => '帕劳',
@@ -16898,6 +17944,8 @@ $zh2CN = array(
'寮國' => '老挝',
'肯雅' => '肯尼亚',
'肯亞' => '肯尼亚',
+'自由球员' => '自由球员',
+'自由球員' => '自由球员',
'單車' => '自行车',
'太空梭' => '航天飞机',
'穿梭機' => '航天飞机',
@@ -16908,7 +17956,6 @@ $zh2CN = array(
'士多啤梨' => '草莓',
'莫三比克' => '莫桑比克',
'賴索托' => '莱索托',
-'海珊' => '萨达姆',
'辭彙' => '词汇',
'片語' => '词组',
'調制解調器' => '调制解调器',
@@ -16954,8 +18001,8 @@ $zh2SG = array(
'方便面' => '快速面',
'零钱' => '散钱',
'散紙' => '散钱',
-'榴莲' => '榴梿',
'榴蓮' => '榴梿',
+'榴莲' => '榴梿',
'笨豬跳' => '绑紧跳',
'蹦极跳' => '绑紧跳',
'笑星' => '谐星',
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php
index 8cf8c096..b703ab4f 100644
--- a/includes/api/ApiBase.php
+++ b/includes/api/ApiBase.php
@@ -1,11 +1,11 @@
<?php
-/*
+/**
* Created on Sep 5, 2006
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ * Copyright © 2006, 2010 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,6 +49,7 @@ abstract class ApiBase {
const PARAM_MAX2 = 4; // Max value allowed for a parameter for bots and sysops. Only applies if TYPE='integer'
const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer'
const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true
+ const PARAM_DEPRECATED = 7; // Boolean, is the parameter deprecated (will show a warning)
const LIMIT_BIG1 = 500; // Fast query, std user limit
const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit
@@ -56,6 +57,7 @@ abstract class ApiBase {
const LIMIT_SML2 = 500; // Slow query, bot/sysop limit
private $mMainModule, $mModuleName, $mModulePrefix;
+ private $mParamCache = array();
/**
* Constructor
@@ -63,7 +65,7 @@ abstract class ApiBase {
* @param $moduleName string Name of this module
* @param $modulePrefix string Prefix to use for parameter names
*/
- public function __construct($mainModule, $moduleName, $modulePrefix = '') {
+ public function __construct( $mainModule, $moduleName, $modulePrefix = '' ) {
$this->mMainModule = $mainModule;
$this->mModuleName = $moduleName;
$this->mModulePrefix = $modulePrefix;
@@ -119,11 +121,12 @@ abstract class ApiBase {
* Get the name of the module as shown in the profiler log
* @return string
*/
- public function getModuleProfileName($db = false) {
- if ($db)
+ public function getModuleProfileName( $db = false ) {
+ if ( $db ) {
return 'API:' . $this->mModuleName . '-DB';
- else
+ } else {
return 'API:' . $this->mModuleName;
+ }
}
/**
@@ -150,8 +153,9 @@ abstract class ApiBase {
public function getResult() {
// Main module has getResult() method overriden
// Safety - avoid infinite loop:
- if ($this->isMain())
- ApiBase :: dieDebug(__METHOD__, 'base method was called on main module. ');
+ if ( $this->isMain() ) {
+ ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' );
+ }
return $this->getMain()->getResult();
}
@@ -170,23 +174,24 @@ abstract class ApiBase {
* newlines
* @param $warning string Warning message
*/
- public function setWarning($warning) {
+ public function setWarning( $warning ) {
$data = $this->getResult()->getData();
- if(isset($data['warnings'][$this->getModuleName()]))
- {
- # Don't add duplicate warnings
- $warn_regex = preg_quote($warning, '/');
- if(preg_match("/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*']))
+ if ( isset( $data['warnings'][$this->getModuleName()] ) ) {
+ // Don't add duplicate warnings
+ $warn_regex = preg_quote( $warning, '/' );
+ if ( preg_match( "/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*'] ) )
+ {
return;
+ }
$oldwarning = $data['warnings'][$this->getModuleName()]['*'];
- # If there is a warning already, append it to the existing one
+ // If there is a warning already, append it to the existing one
$warning = "$oldwarning\n$warning";
- $this->getResult()->unsetValue('warnings', $this->getModuleName());
+ $this->getResult()->unsetValue( 'warnings', $this->getModuleName() );
}
$msg = array();
- ApiResult :: setContent($msg, $warning);
+ ApiResult::setContent( $msg, $warning );
$this->getResult()->disableSizeCheck();
- $this->getResult()->addValue('warnings', $this->getModuleName(), $msg);
+ $this->getResult()->addValue( 'warnings', $this->getModuleName(), $msg );
$this->getResult()->enableSizeCheck();
}
@@ -205,58 +210,65 @@ abstract class ApiBase {
* @return mixed string or false
*/
public function makeHelpMsg() {
-
static $lnPrfx = "\n ";
$msg = $this->getDescription();
- if ($msg !== false) {
+ if ( $msg !== false ) {
- if (!is_array($msg))
- $msg = array (
+ if ( !is_array( $msg ) ) {
+ $msg = array(
$msg
);
- $msg = $lnPrfx . implode($lnPrfx, $msg) . "\n";
+ }
+ $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n";
- if ($this->isReadMode())
+ if ( $this->isReadMode() ) {
$msg .= "\nThis module requires read rights.";
- if ($this->isWriteMode())
+ }
+ if ( $this->isWriteMode() ) {
$msg .= "\nThis module requires write rights.";
- if ($this->mustBePosted())
+ }
+ if ( $this->mustBePosted() ) {
$msg .= "\nThis module only accepts POST requests.";
- if ($this->isReadMode() || $this->isWriteMode() ||
- $this->mustBePosted())
+ }
+ if ( $this->isReadMode() || $this->isWriteMode() ||
+ $this->mustBePosted() )
+ {
$msg .= "\n";
+ }
// Parameters
$paramsMsg = $this->makeHelpMsgParameters();
- if ($paramsMsg !== false) {
+ if ( $paramsMsg !== false ) {
$msg .= "Parameters:\n$paramsMsg";
}
// Examples
$examples = $this->getExamples();
- if ($examples !== false) {
- if (!is_array($examples))
- $examples = array (
+ if ( $examples !== false ) {
+ if ( !is_array( $examples ) ) {
+ $examples = array(
$examples
);
- $msg .= 'Example' . (count($examples) > 1 ? 's' : '') . ":\n ";
- $msg .= implode($lnPrfx, $examples) . "\n";
+ }
+ $msg .= 'Example' . ( count( $examples ) > 1 ? 's' : '' ) . ":\n ";
+ $msg .= implode( $lnPrfx, $examples ) . "\n";
}
- if ($this->getMain()->getShowVersions()) {
+ if ( $this->getMain()->getShowVersions() ) {
$versions = $this->getVersion();
$pattern = '/(\$.*) ([0-9a-z_]+\.php) (.*\$)/i';
- $replacement = '\\0' . "\n " . 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/api/\\2';
+ $callback = array( $this, 'makeHelpMsg_callback' );
- if (is_array($versions)) {
- foreach ($versions as &$v)
- $v = preg_replace($pattern, $replacement, $v);
- $versions = implode("\n ", $versions);
+ if ( is_array( $versions ) ) {
+ foreach ( $versions as &$v ) {
+ $v = preg_replace_callback( $pattern, $callback, $v );
+ }
+ $versions = implode( "\n ", $versions );
+ } else {
+ $versions = preg_replace_callback( $pattern, $callback, $versions );
}
- else
- $versions = preg_replace($pattern, $replacement, $versions);
$msg .= "Version:\n $versions\n";
}
@@ -272,51 +284,61 @@ abstract class ApiBase {
*/
public function makeHelpMsgParameters() {
$params = $this->getFinalParams();
- if ($params !== false) {
+ if ( $params ) {
$paramsDescription = $this->getFinalParamDescription();
$msg = '';
- $paramPrefix = "\n" . str_repeat(' ', 19);
- foreach ($params as $paramName => $paramSettings) {
- $desc = isset ($paramsDescription[$paramName]) ? $paramsDescription[$paramName] : '';
- if (is_array($desc))
- $desc = implode($paramPrefix, $desc);
-
- $type = isset($paramSettings[self :: PARAM_TYPE])? $paramSettings[self :: PARAM_TYPE] : null;
- if (isset ($type)) {
- if (isset ($paramSettings[self :: PARAM_ISMULTI]))
+ $paramPrefix = "\n" . str_repeat( ' ', 19 );
+ foreach ( $params as $paramName => $paramSettings ) {
+ $desc = isset( $paramsDescription[$paramName] ) ? $paramsDescription[$paramName] : '';
+ if ( is_array( $desc ) ) {
+ $desc = implode( $paramPrefix, $desc );
+ }
+
+ $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) ?
+ $paramSettings[self::PARAM_DEPRECATED] : false;
+ if ( $deprecated ) {
+ $desc = "DEPRECATED! $desc";
+ }
+
+ $type = isset( $paramSettings[self::PARAM_TYPE] ) ? $paramSettings[self::PARAM_TYPE] : null;
+ if ( isset( $type ) ) {
+ if ( isset( $paramSettings[self::PARAM_ISMULTI] ) ) {
$prompt = 'Values (separate with \'|\'): ';
- else
+ } else {
$prompt = 'One value: ';
+ }
- if (is_array($type)) {
+ if ( is_array( $type ) ) {
$choices = array();
$nothingPrompt = false;
- foreach ($type as $t)
- if ($t === '')
+ foreach ( $type as $t )
+ if ( $t === '' ) {
$nothingPrompt = 'Can be empty, or ';
- else
+ } else {
$choices[] = $t;
- $desc .= $paramPrefix . $nothingPrompt . $prompt . implode(', ', $choices);
+ }
+ $desc .= $paramPrefix . $nothingPrompt . $prompt . implode( ', ', $choices );
} else {
- switch ($type) {
+ switch ( $type ) {
case 'namespace':
// Special handling because namespaces are type-limited, yet they are not given
- $desc .= $paramPrefix . $prompt . implode(', ', ApiBase :: getValidNamespaces());
+ $desc .= $paramPrefix . $prompt . implode( ', ', ApiBase::getValidNamespaces() );
break;
case 'limit':
- $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]} ({$paramSettings[self :: PARAM_MAX2]} for bots) allowed.";
+ $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]} ({$paramSettings[self::PARAM_MAX2]} for bots) allowed.";
break;
case 'integer':
- $hasMin = isset($paramSettings[self :: PARAM_MIN]);
- $hasMax = isset($paramSettings[self :: PARAM_MAX]);
- if ($hasMin || $hasMax) {
- if (!$hasMax)
- $intRangeStr = "The value must be no less than {$paramSettings[self :: PARAM_MIN]}";
- elseif (!$hasMin)
- $intRangeStr = "The value must be no more than {$paramSettings[self :: PARAM_MAX]}";
- else
- $intRangeStr = "The value must be between {$paramSettings[self :: PARAM_MIN]} and {$paramSettings[self :: PARAM_MAX]}";
+ $hasMin = isset( $paramSettings[self::PARAM_MIN] );
+ $hasMax = isset( $paramSettings[self::PARAM_MAX] );
+ if ( $hasMin || $hasMax ) {
+ if ( !$hasMax ) {
+ $intRangeStr = "The value must be no less than {$paramSettings[self::PARAM_MIN]}";
+ } elseif ( !$hasMin ) {
+ $intRangeStr = "The value must be no more than {$paramSettings[self::PARAM_MAX]}";
+ } else {
+ $intRangeStr = "The value must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}";
+ }
$desc .= $paramPrefix . $intRangeStr;
}
@@ -325,16 +347,51 @@ abstract class ApiBase {
}
}
- $default = is_array($paramSettings) ? (isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null) : $paramSettings;
- if (!is_null($default) && $default !== false)
+ $default = is_array( $paramSettings ) ? ( isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null ) : $paramSettings;
+ if ( !is_null( $default ) && $default !== false ) {
$desc .= $paramPrefix . "Default: $default";
+ }
- $msg .= sprintf(" %-14s - %s\n", $this->encodeParamName($paramName), $desc);
+ $msg .= sprintf( " %-14s - %s\n", $this->encodeParamName( $paramName ), $desc );
}
return $msg;
- } else
+ } else {
return false;
+ }
+ }
+
+ /**
+ * Callback for preg_replace_callback() call in makeHelpMsg().
+ * Replaces a source file name with a link to ViewVC
+ */
+ public function makeHelpMsg_callback( $matches ) {
+ global $wgAutoloadClasses, $wgAutoloadLocalClasses;
+ if ( isset( $wgAutoloadLocalClasses[get_class( $this )] ) ) {
+ $file = $wgAutoloadLocalClasses[get_class( $this )];
+ } elseif ( isset( $wgAutoloadClasses[get_class( $this )] ) ) {
+ $file = $wgAutoloadClasses[get_class( $this )];
+ }
+
+ // Do some guesswork here
+ $path = strstr( $file, 'includes/api/' );
+ if ( $path === false ) {
+ $path = strstr( $file, 'extensions/' );
+ } else {
+ $path = 'phase3/' . $path;
+ }
+
+ // Get the filename from $matches[2] instead of $file
+ // If they're not the same file, they're assumed to be in the
+ // same directory
+ // This is necessary to make stuff like ApiMain::getVersion()
+ // returning the version string for ApiBase work
+ if ( $path ) {
+ return "{$matches[0]}\n http://svn.wikimedia.org/" .
+ "viewvc/mediawiki/trunk/" . dirname( $path ) .
+ "/{$matches[2]}";
+ }
+ return $matches[0];
}
/**
@@ -373,7 +430,7 @@ abstract class ApiBase {
protected function getParamDescription() {
return false;
}
-
+
/**
* Get final list of parameters, after hooks have had a chance to
* tweak it as needed.
@@ -381,7 +438,7 @@ abstract class ApiBase {
*/
public function getFinalParams() {
$params = $this->getAllowedParams();
- wfRunHooks('APIGetAllowedParams', array(&$this, &$params));
+ wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params ) );
return $params;
}
@@ -392,7 +449,7 @@ abstract class ApiBase {
*/
public function getFinalParamDescription() {
$desc = $this->getParamDescription();
- wfRunHooks('APIGetParamDescription', array(&$this, &$desc));
+ wfRunHooks( 'APIGetParamDescription', array( &$this, &$desc ) );
return $desc;
}
@@ -402,56 +459,63 @@ abstract class ApiBase {
* @param $paramName string Parameter name
* @return string Prefixed parameter name
*/
- public function encodeParamName($paramName) {
+ public function encodeParamName( $paramName ) {
return $this->mModulePrefix . $paramName;
}
/**
- * Using getAllowedParams(), this function makes an array of the values
- * provided by the user, with key being the name of the variable, and
- * value - validated value from user or default. limit=max will not be
- * parsed if $parseMaxLimit is set to false; use this when the max
- * limit is not definitive yet, e.g. when getting revisions.
- * @param $parseMaxLimit bool
- * @return array
- */
- public function extractRequestParams($parseMaxLimit = true) {
- $params = $this->getFinalParams();
- $results = array ();
-
- foreach ($params as $paramName => $paramSettings)
- $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
-
- return $results;
+ * Using getAllowedParams(), this function makes an array of the values
+ * provided by the user, with key being the name of the variable, and
+ * value - validated value from user or default. limits will not be
+ * parsed if $parseLimit is set to false; use this when the max
+ * limit is not definitive yet, e.g. when getting revisions.
+ * @param $parseLimit Boolean: true by default
+ * @return array
+ */
+ public function extractRequestParams( $parseLimit = true ) {
+ // Cache parameters, for performance and to avoid bug 24564.
+ if ( !isset( $this->mParamCache[$parseLimit] ) ) {
+ $params = $this->getFinalParams();
+ $results = array();
+
+ if ( $params ) { // getFinalParams() can return false
+ foreach ( $params as $paramName => $paramSettings ) {
+ $results[$paramName] = $this->getParameterFromSettings(
+ $paramName, $paramSettings, $parseLimit );
+ }
+ }
+ $this->mParamCache[$parseLimit] = $results;
+ }
+ return $this->mParamCache[$parseLimit];
}
/**
* Get a value for the given parameter
* @param $paramName string Parameter name
- * @param $parseMaxLimit bool see extractRequestParams()
+ * @param $parseLimit bool see extractRequestParams()
* @return mixed Parameter value
*/
- protected function getParameter($paramName, $parseMaxLimit = true) {
+ protected function getParameter( $paramName, $parseLimit = true ) {
$params = $this->getFinalParams();
$paramSettings = $params[$paramName];
- return $this->getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit);
+ return $this->getParameterFromSettings( $paramName, $paramSettings, $parseLimit );
}
-
+
/**
- * Die if none or more than one of a certain set of parameters is set
+ * Die if none or more than one of a certain set of parameters is set and not false.
* @param $params array of parameter names
*/
- public function requireOnlyOneParameter($params) {
+ public function requireOnlyOneParameter( $params ) {
$required = func_get_args();
- array_shift($required);
-
- $intersection = array_intersect(array_keys(array_filter($params,
- create_function('$x', 'return !is_null($x);')
- )), $required);
- if (count($intersection) > 1) {
- $this->dieUsage('The parameters '.implode(', ', $intersection).' can not be used together', 'invalidparammix');
- } elseif (count($intersection) == 0) {
- $this->dieUsage('One of the parameters '.implode(', ', $required).' is required', 'missingparam');
+ array_shift( $required );
+
+ $intersection = array_intersect( array_keys( array_filter( $params,
+ create_function( '$x', 'return !is_null($x) && $x !== false;' )
+ ) ), $required );
+ if ( count( $intersection ) > 1 ) {
+ $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' );
+ } elseif ( count( $intersection ) == 0 ) {
+ $this->dieUsage( 'One of the parameters ' . implode( ', ', $required ) . ' is required', 'missingparam' );
}
}
@@ -462,15 +526,17 @@ abstract class ApiBase {
*/
public static function getValidNamespaces() {
static $mValidNamespaces = null;
- if (is_null($mValidNamespaces)) {
+ if ( is_null( $mValidNamespaces ) ) {
global $wgContLang;
- $mValidNamespaces = array ();
- foreach (array_keys($wgContLang->getNamespaces()) as $ns) {
- if ($ns >= 0)
+ $mValidNamespaces = array();
+ foreach ( array_keys( $wgContLang->getNamespaces() ) as $ns ) {
+ if ( $ns >= 0 ) {
$mValidNamespaces[] = $ns;
+ }
}
}
+
return $mValidNamespaces;
}
@@ -480,163 +546,183 @@ abstract class ApiBase {
* @param $paramName String: parameter name
* @param $paramSettings Mixed: default value or an array of settings
* using PARAM_* constants.
- * @param $parseMaxLimit Boolean: parse limit when max is given?
+ * @param $parseLimit Boolean: parse limit?
* @return mixed Parameter value
*/
- protected function getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit) {
-
+ protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) {
// Some classes may decide to change parameter names
- $encParamName = $this->encodeParamName($paramName);
+ $encParamName = $this->encodeParamName( $paramName );
- if (!is_array($paramSettings)) {
+ if ( !is_array( $paramSettings ) ) {
$default = $paramSettings;
$multi = false;
- $type = gettype($paramSettings);
+ $type = gettype( $paramSettings );
$dupes = false;
+ $deprecated = false;
} else {
- $default = isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null;
- $multi = isset ($paramSettings[self :: PARAM_ISMULTI]) ? $paramSettings[self :: PARAM_ISMULTI] : false;
- $type = isset ($paramSettings[self :: PARAM_TYPE]) ? $paramSettings[self :: PARAM_TYPE] : null;
- $dupes = isset ($paramSettings[self:: PARAM_ALLOW_DUPLICATES]) ? $paramSettings[self :: PARAM_ALLOW_DUPLICATES] : false;
+ $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null;
+ $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) ? $paramSettings[self::PARAM_ISMULTI] : false;
+ $type = isset( $paramSettings[self::PARAM_TYPE] ) ? $paramSettings[self::PARAM_TYPE] : null;
+ $dupes = isset( $paramSettings[self::PARAM_ALLOW_DUPLICATES] ) ? $paramSettings[self::PARAM_ALLOW_DUPLICATES] : false;
+ $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) ? $paramSettings[self::PARAM_DEPRECATED] : false;
// When type is not given, and no choices, the type is the same as $default
- if (!isset ($type)) {
- if (isset ($default))
- $type = gettype($default);
- else
+ if ( !isset( $type ) ) {
+ if ( isset( $default ) ) {
+ $type = gettype( $default );
+ } else {
$type = 'NULL'; // allow everything
+ }
}
}
- if ($type == 'boolean') {
- if (isset ($default) && $default !== false) {
+ if ( $type == 'boolean' ) {
+ if ( isset( $default ) && $default !== false ) {
// Having a default value of anything other than 'false' is pointless
- ApiBase :: dieDebug(__METHOD__, "Boolean param $encParamName's default is set to '$default'");
+ ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'" );
}
- $value = $this->getMain()->getRequest()->getCheck($encParamName);
+ $value = $this->getMain()->getRequest()->getCheck( $encParamName );
} else {
- $value = $this->getMain()->getRequest()->getVal($encParamName, $default);
+ $value = $this->getMain()->getRequest()->getVal( $encParamName, $default );
- if (isset ($value) && $type == 'namespace')
- $type = ApiBase :: getValidNamespaces();
+ if ( isset( $value ) && $type == 'namespace' ) {
+ $type = ApiBase::getValidNamespaces();
+ }
}
- if (isset ($value) && ($multi || is_array($type)))
- $value = $this->parseMultiValue($encParamName, $value, $multi, is_array($type) ? $type : null);
+ if ( isset( $value ) && ( $multi || is_array( $type ) ) ) {
+ $value = $this->parseMultiValue( $encParamName, $value, $multi, is_array( $type ) ? $type : null );
+ }
// More validation only when choices were not given
// choices were validated in parseMultiValue()
- if (isset ($value)) {
- if (!is_array($type)) {
- switch ($type) {
- case 'NULL' : // nothing to do
+ if ( isset( $value ) ) {
+ if ( !is_array( $type ) ) {
+ switch ( $type ) {
+ case 'NULL': // nothing to do
break;
- case 'string' : // nothing to do
+ case 'string': // nothing to do
break;
- case 'integer' : // Force everything using intval() and optionally validate limits
+ case 'integer': // Force everything using intval() and optionally validate limits
- $value = is_array($value) ? array_map('intval', $value) : intval($value);
- $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : null;
- $max = isset ($paramSettings[self :: PARAM_MAX]) ? $paramSettings[self :: PARAM_MAX] : null;
+ $value = is_array( $value ) ? array_map( 'intval', $value ) : intval( $value );
+ $min = isset ( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null;
+ $max = isset ( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null;
- if (!is_null($min) || !is_null($max)) {
- $values = is_array($value) ? $value : array($value);
- foreach ($values as $v) {
- $this->validateLimit($paramName, $v, $min, $max);
+ if ( !is_null( $min ) || !is_null( $max ) ) {
+ $values = is_array( $value ) ? $value : array( $value );
+ foreach ( $values as &$v ) {
+ $this->validateLimit( $paramName, $v, $min, $max );
}
}
break;
- case 'limit' :
- if (!isset ($paramSettings[self :: PARAM_MAX]) || !isset ($paramSettings[self :: PARAM_MAX2]))
- ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $encParamName");
- if ($multi)
- ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
- $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0;
- if( $value == 'max' ) {
- if( $parseMaxLimit ) {
- $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self :: PARAM_MAX2] : $paramSettings[self :: PARAM_MAX];
- $this->getResult()->addValue( 'limits', $this->getModuleName(), $value );
- $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
- }
+ case 'limit':
+ if ( !$parseLimit ) {
+ // Don't do any validation whatsoever
+ break;
+ }
+ if ( !isset( $paramSettings[self::PARAM_MAX] ) || !isset( $paramSettings[self::PARAM_MAX2] ) ) {
+ ApiBase::dieDebug( __METHOD__, "MAX1 or MAX2 are not defined for the limit $encParamName" );
}
- else {
- $value = intval($value);
- $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX], $paramSettings[self :: PARAM_MAX2]);
+ if ( $multi ) {
+ ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
+ }
+ $min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : 0;
+ if ( $value == 'max' ) {
+ $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self::PARAM_MAX2] : $paramSettings[self::PARAM_MAX];
+ $this->getResult()->addValue( 'limits', $this->getModuleName(), $value );
+ } else {
+ $value = intval( $value );
+ $this->validateLimit( $paramName, $value, $min, $paramSettings[self::PARAM_MAX], $paramSettings[self::PARAM_MAX2] );
}
break;
- case 'boolean' :
- if ($multi)
- ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
+ case 'boolean':
+ if ( $multi )
+ ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
break;
- case 'timestamp' :
- if ($multi)
- ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $encParamName");
- $value = wfTimestamp(TS_UNIX, $value);
- if ($value === 0)
- $this->dieUsage("Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}");
- $value = wfTimestamp(TS_MW, $value);
+ case 'timestamp':
+ if ( $multi ) {
+ ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" );
+ }
+ $value = wfTimestamp( TS_UNIX, $value );
+ if ( $value === 0 ) {
+ $this->dieUsage( "Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}" );
+ }
+ $value = wfTimestamp( TS_MW, $value );
break;
- case 'user' :
+ case 'user':
$title = Title::makeTitleSafe( NS_USER, $value );
- if ( is_null( $title ) )
- $this->dieUsage("Invalid value for user parameter $encParamName", "baduser_{$encParamName}");
+ if ( is_null( $title ) ) {
+ $this->dieUsage( "Invalid value for user parameter $encParamName", "baduser_{$encParamName}" );
+ }
$value = $title->getText();
break;
- default :
- ApiBase :: dieDebug(__METHOD__, "Param $encParamName's type is unknown - $type");
+ default:
+ ApiBase::dieDebug( __METHOD__, "Param $encParamName's type is unknown - $type" );
}
}
// Throw out duplicates if requested
- if (is_array($value) && !$dupes)
- $value = array_unique($value);
+ if ( is_array( $value ) && !$dupes ) {
+ $value = array_unique( $value );
+ }
+
+ // Set a warning if a deprecated parameter has been passed
+ if ( $deprecated && $value !== false ) {
+ $this->setWarning( "The $encParamName parameter has been deprecated." );
+ }
}
return $value;
}
/**
- * Return an array of values that were given in a 'a|b|c' notation,
- * after it optionally validates them against the list allowed values.
- *
- * @param $valueName string The name of the parameter (for error
- * reporting)
- * @param $value mixed The value being parsed
- * @param $allowMultiple bool Can $value contain more than one value
- * separated by '|'?
- * @param $allowedValues mixed An array of values to check against. If
- * null, all values are accepted.
- * @return mixed (allowMultiple ? an_array_of_values : a_single_value)
- */
- protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) {
- if( trim($value) === "" && $allowMultiple)
+ * 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 string The name of the parameter (for error
+ * reporting)
+ * @param $value mixed The value being parsed
+ * @param $allowMultiple bool Can $value contain more than one value
+ * separated by '|'?
+ * @param $allowedValues mixed An array of values to check against. If
+ * null, all values are accepted.
+ * @return mixed (allowMultiple ? an_array_of_values : a_single_value)
+ */
+ protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) {
+ if ( trim( $value ) === '' && $allowMultiple ) {
return array();
- $sizeLimit = $this->mMainModule->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1;
- $valuesList = explode('|', $value, $sizeLimit + 1);
- if( self::truncateArray($valuesList, $sizeLimit) ) {
- $this->setWarning("Too many values supplied for parameter '$valueName': the limit is $sizeLimit");
- }
- if (!$allowMultiple && count($valuesList) != 1) {
- $possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : '';
- $this->dieUsage("Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName");
- }
- if (is_array($allowedValues)) {
- # Check for unknown values
- $unknown = array_diff($valuesList, $allowedValues);
- if(count($unknown))
- {
- if($allowMultiple)
- {
- $s = count($unknown) > 1 ? "s" : "";
- $vals = implode(", ", $unknown);
- $this->setWarning("Unrecognized value$s for parameter '$valueName': $vals");
+ }
+
+ // This is a bit awkward, but we want to avoid calling canApiHighLimits() because it unstubs $wgUser
+ $valuesList = explode( '|', $value, self::LIMIT_SML2 + 1 );
+ $sizeLimit = count( $valuesList ) > self::LIMIT_SML1 && $this->mMainModule->canApiHighLimits() ?
+ self::LIMIT_SML2 : self::LIMIT_SML1;
+
+ if ( self::truncateArray( $valuesList, $sizeLimit ) ) {
+ $this->setWarning( "Too many values supplied for parameter '$valueName': the limit is $sizeLimit" );
+ }
+
+ if ( !$allowMultiple && count( $valuesList ) != 1 ) {
+ $possibleValues = is_array( $allowedValues ) ? "of '" . implode( "', '", $allowedValues ) . "'" : '';
+ $this->dieUsage( "Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName" );
+ }
+
+ if ( is_array( $allowedValues ) ) {
+ // Check for unknown values
+ $unknown = array_diff( $valuesList, $allowedValues );
+ if ( count( $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" );
}
- else
- $this->dieUsage("Unrecognized value for parameter '$valueName': {$valuesList[0]}", "unknown_$valueName");
}
- # Now throw them out
- $valuesList = array_intersect($valuesList, $allowedValues);
+ // Now throw them out
+ $valuesList = array_intersect( $valuesList, $allowedValues );
}
return $allowMultiple ? $valuesList : $valuesList[0];
@@ -651,54 +737,61 @@ abstract class ApiBase {
* @param $max int Maximum value for users
* @param $botMax int Maximum value for sysops/bots
*/
- function validateLimit($paramName, $value, $min, $max, $botMax = null) {
- if (!is_null($min) && $value < $min) {
- $this->dieUsage($this->encodeParamName($paramName) . " may not be less than $min (set to $value)", $paramName);
+ function validateLimit( $paramName, &$value, $min, $max, $botMax = null ) {
+ if ( !is_null( $min ) && $value < $min ) {
+ $this->setWarning( $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)" );
+ $value = $min;
}
// Minimum is always validated, whereas maximum is checked only if not running in internal call mode
- if ($this->getMain()->isInternalMode())
+ if ( $this->getMain()->isInternalMode() ) {
return;
+ }
// Optimization: do not check user's bot status unless really needed -- skips db query
// assumes $botMax >= $max
- if (!is_null($max) && $value > $max) {
- if (!is_null($botMax) && $this->getMain()->canApiHighLimits()) {
- if ($value > $botMax) {
- $this->dieUsage($this->encodeParamName($paramName) . " may not be over $botMax (set to $value) for bots or sysops", $paramName);
+ if ( !is_null( $max ) && $value > $max ) {
+ if ( !is_null( $botMax ) && $this->getMain()->canApiHighLimits() ) {
+ if ( $value > $botMax ) {
+ $this->setWarning( $this->encodeParamName( $paramName ) . " may not be over $botMax (set to $value) for bots or sysops" );
+ $value = $botMax;
}
} else {
- $this->dieUsage($this->encodeParamName($paramName) . " may not be over $max (set to $value) for users", $paramName);
+ $this->setWarning( $this->encodeParamName( $paramName ) . " may not be over $max (set to $value) for users" );
+ $value = $max;
}
}
}
-
+
/**
* Truncate an array to a certain length.
* @param $arr array Array to truncate
* @param $limit int Maximum length
* @return bool True if the array was truncated, false otherwise
*/
- public static function truncateArray(&$arr, $limit)
- {
+ public static function truncateArray( &$arr, $limit ) {
$modified = false;
- while(count($arr) > $limit)
- {
- $junk = array_pop($arr);
+ while ( count( $arr ) > $limit ) {
+ $junk = array_pop( $arr );
$modified = true;
}
return $modified;
}
/**
- * Call the main module's error handler
- * @param $description string Error text
- * @param $errorCode string Error code
+ * Throw a UsageException, which will (if uncaught) call the main module's
+ * error handler and die with an error message.
+ *
+ * @param $description string One-line human-readable description of the
+ * error condition, e.g., "The API requires a valid action parameter"
+ * @param $errorCode string Brief, arbitrary, stable string to allow easy
+ * automated identification of the error, e.g., 'unknown_action'
* @param $httpRespCode int HTTP response code
+ * @param $extradata array Data to add to the <error> element; array in ApiResult format
*/
- public function dieUsage($description, $errorCode, $httpRespCode = 0) {
+ public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) {
wfProfileClose();
- throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode);
+ throw new UsageException( $description, $this->encodeParamName( $errorCode ), $httpRespCode, $extradata );
}
/**
@@ -706,145 +799,170 @@ abstract class ApiBase {
*/
public static $messageMap = array(
// 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'),
+ '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"),
- 'namespaceprotected' => array('code' => 'protectednamespace', 'info' => "You're not allowed to edit pages in the ``\$1'' namespace"),
- 'customcssjsprotected' => array('code' => 'customcssjsprotected', 'info' => "You're not allowed to edit custom CSS and JavaScript pages"),
- 'cascadeprotected' => array('code' => 'cascadeprotected', 'info' =>"The page you're trying to edit is protected because it's included in a cascade-protected page"),
- 'protectedpagetext' => array('code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page"),
- 'protect-cantedit' => array('code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it"),
- 'badaccess-group0' => array('code' => 'permissiondenied', 'info' => "Permission denied"), // Generic permission denied message
- 'badaccess-groups' => array('code' => 'permissiondenied', 'info' => "Permission denied"),
- 'titleprotected' => array('code' => 'protectedtitle', 'info' => "This title has been protected from creation"),
- 'nocreate-loggedin' => array('code' => 'cantcreate', 'info' => "You don't have permission to create new pages"),
- 'nocreatetext' => array('code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages"),
- 'movenologintext' => array('code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages"),
- 'movenotallowed' => array('code' => 'cantmove', 'info' => "You don't have permission to move pages"),
- '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"),
+ '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" ),
+ 'namespaceprotected' => array( 'code' => 'protectednamespace', 'info' => "You're not allowed to edit pages in the ``\$1'' namespace" ),
+ 'customcssjsprotected' => array( 'code' => 'customcssjsprotected', 'info' => "You're not allowed to edit custom CSS and JavaScript pages" ),
+ 'cascadeprotected' => array( 'code' => 'cascadeprotected', 'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page" ),
+ 'protectedpagetext' => array( 'code' => 'protectedpage', 'info' => "The ``\$1'' right is required to edit this page" ),
+ 'protect-cantedit' => array( 'code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it" ),
+ 'badaccess-group0' => array( 'code' => 'permissiondenied', 'info' => "Permission denied" ), // Generic permission denied message
+ 'badaccess-groups' => array( 'code' => 'permissiondenied', 'info' => "Permission denied" ),
+ 'titleprotected' => array( 'code' => 'protectedtitle', 'info' => "This title has been protected from creation" ),
+ 'nocreate-loggedin' => array( 'code' => 'cantcreate', 'info' => "You don't have permission to create new pages" ),
+ 'nocreatetext' => array( 'code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages" ),
+ 'movenologintext' => array( 'code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages" ),
+ 'movenotallowed' => array( 'code' => 'cantmove', 'info' => "You don't have permission to move pages" ),
+ '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"),
- '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"),
- 'notanarticle' => array('code' => 'missingtitle', 'info' => "The page you requested doesn't exist"),
- 'selfmove' => array('code' => 'selfmove', 'info' => "Can't move a page to itself"),
- 'immobile_namespace' => array('code' => 'immobilenamespace', 'info' => "You tried to move pages from or to a namespace that is protected from moving"),
- 'articleexists' => array('code' => 'articleexists', 'info' => "The destination article already exists and is not a redirect to the source article"),
- '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"),
+ '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" ),
+ '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" ),
+ 'notanarticle' => array( 'code' => 'missingtitle', 'info' => "The page you requested doesn't exist" ),
+ 'selfmove' => array( 'code' => 'selfmove', 'info' => "Can't move a page to itself" ),
+ 'immobile_namespace' => array( 'code' => 'immobilenamespace', 'info' => "You tried to move pages from or to a namespace that is protected from moving" ),
+ 'articleexists' => array( 'code' => 'articleexists', 'info' => "The destination article already exists and is not a redirect to the source article" ),
+ '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"),
- 'range_block_disabled' => array('code' => 'rangedisabled', 'info' => "Blocking IP ranges has been disabled"),
- 'nosuchusershort' => array('code' => 'nosuchuser', 'info' => "The user you specified doesn't exist"),
- 'badipaddress' => array('code' => 'invalidip', 'info' => "Invalid IP address specified"),
- 'ipb_expiry_invalid' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time"),
- 'ipb_already_blocked' => array('code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked"),
- 'ipb_blocked_as_range' => array('code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole."),
- 'ipb_cant_unblock' => array('code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already"),
- 'mailnologin' => array('code' => 'cantsend', 'info' => "You're not logged in or you don't have a confirmed e-mail address, so you can't send e-mail"),
- 'usermaildisabled' => array('code' => 'usermaildisabled', 'info' => "User email has been disabled"),
- 'blockedemailuser' => array('code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail"),
- 'notarget' => array('code' => 'notarget', 'info' => "You have not specified a valid target for this action"),
- 'noemail' => array('code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users"),
- 'rcpatroldisabled' => array('code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki"),
- 'markedaspatrollederror-noautopatrol' => array('code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes"),
- 'delete-toobig' => array('code' => 'bigdelete', 'info' => "You can't delete this page because it has more than \$1 revisions"),
- 'movenotallowedfile' => array('code' => 'cantmovefile', 'info' => "You don't have permission to move files"),
+ 'ip_range_invalid' => array( 'code' => 'invalidrange', 'info' => "Invalid IP range" ),
+ 'range_block_disabled' => array( 'code' => 'rangedisabled', 'info' => "Blocking IP ranges has been disabled" ),
+ 'nosuchusershort' => array( 'code' => 'nosuchuser', 'info' => "The user you specified doesn't exist" ),
+ 'badipaddress' => array( 'code' => 'invalidip', 'info' => "Invalid IP address specified" ),
+ 'ipb_expiry_invalid' => array( 'code' => 'invalidexpiry', 'info' => "Invalid expiry time" ),
+ 'ipb_already_blocked' => array( 'code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked" ),
+ 'ipb_blocked_as_range' => array( 'code' => 'blockedasrange', 'info' => "IP address ``\$1'' was blocked as part of range ``\$2''. You can't unblock the IP invidually, but you can unblock the range as a whole." ),
+ 'ipb_cant_unblock' => array( 'code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already" ),
+ 'mailnologin' => array( 'code' => 'cantsend', 'info' => "You are not logged in, you do not have a confirmed e-mail address, or you are not allowed to send e-mail to other users, so you cannot send e-mail" ),
+ 'usermaildisabled' => array( 'code' => 'usermaildisabled', 'info' => "User email has been disabled" ),
+ 'blockedemailuser' => array( 'code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail" ),
+ 'notarget' => array( 'code' => 'notarget', 'info' => "You have not specified a valid target for this action" ),
+ 'noemail' => array( 'code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users" ),
+ 'rcpatroldisabled' => array( 'code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki" ),
+ 'markedaspatrollederror-noautopatrol' => array( 'code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes" ),
+ 'delete-toobig' => array( 'code' => 'bigdelete', 'info' => "You can't delete this page because it has more than \$1 revisions" ),
+ 'movenotallowedfile' => array( 'code' => 'cantmovefile', 'info' => "You don't have permission to move files" ),
+ 'userrights-no-interwiki' => array( 'code' => 'nointerwikiuserrights', 'info' => "You don't have permission to change user rights on other wikis" ),
+ 'userrights-nodatabase' => array( 'code' => 'nosuchdatabase', 'info' => "Database ``\$1'' does not exist or is not local" ),
+ 'nouserspecified' => array( 'code' => 'invaliduser', 'info' => "Invalid username ``\$1''" ),
+ 'noname' => array( 'code' => 'invaliduser', 'info' => "Invalid username ``\$1''" ),
// API-specific messages
- 'readrequired' => array('code' => 'readapidenied', 'info' => "You need read permission to use this module"),
- 'writedisabled' => array('code' => 'noapiwrite', 'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file"),
- 'writerequired' => array('code' => 'writeapidenied', 'info' => "You're not allowed to edit this wiki through the API"),
- 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"),
- 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"),
- 'nosuchpageid' => array('code' => 'nosuchpageid', 'info' => "There is no page with ID \$1"),
- 'nosuchrevid' => array('code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1"),
- 'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"),
- 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time ``\$1''"),
- 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time ``\$1'' is in the past"),
- 'create-titleexists' => array('code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'"),
- 'missingtitle-createonly' => array('code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'"),
- 'cantblock' => array('code' => 'cantblock', 'info' => "You don't have permission to block users"),
- '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"),
- '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"),
- 'nosuchrcid' => array('code' => 'nosuchrcid', 'info' => "There is no change with rcid ``\$1''"),
- 'cantpurge' => array('code' => 'cantpurge', 'info' => "Only users with the 'purge' right can purge pages via the API"),
- 'protect-invalidaction' => array('code' => 'protect-invalidaction', 'info' => "Invalid protection type ``\$1''"),
- 'protect-invalidlevel' => array('code' => 'protect-invalidlevel', 'info' => "Invalid protection level ``\$1''"),
- 'toofewexpiries' => array('code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed"),
- 'cantimport' => array('code' => 'cantimport', 'info' => "You don't have permission to import pages"),
- 'cantimport-upload' => array('code' => 'cantimport-upload', 'info' => "You don't have permission to import uploaded pages"),
- 'importnofile' => array('code' => 'nofile', 'info' => "You didn't upload a file"),
- 'importuploaderrorsize' => array('code' => 'filetoobig', 'info' => 'The file you uploaded is bigger than the maximum upload size'),
- 'importuploaderrorpartial' => array('code' => 'partialupload', 'info' => 'The file was only partially uploaded'),
- 'importuploaderrortemp' => array('code' => 'notempdir', 'info' => 'The temporary upload directory is missing'),
- 'importcantopen' => array('code' => 'cantopenfile', 'info' => "Couldn't open the uploaded file"),
- 'import-noarticle' => array('code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified'),
- 'importbadinterwiki' => array('code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified'),
- 'import-unknownerror' => array('code' => 'import-unknownerror', 'info' => "Unknown error on import: ``\$1''"),
+ 'readrequired' => array( 'code' => 'readapidenied', 'info' => "You need read permission to use this module" ),
+ 'writedisabled' => array( 'code' => 'noapiwrite', 'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file" ),
+ 'writerequired' => array( 'code' => 'writeapidenied', 'info' => "You're not allowed to edit this wiki through the API" ),
+ 'missingparam' => array( 'code' => 'no$1', 'info' => "The \$1 parameter must be set" ),
+ 'invalidtitle' => array( 'code' => 'invalidtitle', 'info' => "Bad title ``\$1''" ),
+ 'nosuchpageid' => array( 'code' => 'nosuchpageid', 'info' => "There is no page with ID \$1" ),
+ 'nosuchrevid' => array( 'code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1" ),
+ 'nosuchuser' => array( 'code' => 'nosuchuser', 'info' => "User ``\$1'' doesn't exist" ),
+ 'invaliduser' => array( 'code' => 'invaliduser', 'info' => "Invalid username ``\$1''" ),
+ 'invalidexpiry' => array( 'code' => 'invalidexpiry', 'info' => "Invalid expiry time ``\$1''" ),
+ 'pastexpiry' => array( 'code' => 'pastexpiry', 'info' => "Expiry time ``\$1'' is in the past" ),
+ 'create-titleexists' => array( 'code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'" ),
+ 'missingtitle-createonly' => array( 'code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'" ),
+ 'cantblock' => array( 'code' => 'cantblock', 'info' => "You don't have permission to block users" ),
+ '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" ),
+ '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" ),
+ 'nosuchrcid' => array( 'code' => 'nosuchrcid', 'info' => "There is no change with rcid ``\$1''" ),
+ 'cantpurge' => array( 'code' => 'cantpurge', 'info' => "Only users with the 'purge' right can purge pages via the API" ),
+ 'protect-invalidaction' => array( 'code' => 'protect-invalidaction', 'info' => "Invalid protection type ``\$1''" ),
+ 'protect-invalidlevel' => array( 'code' => 'protect-invalidlevel', 'info' => "Invalid protection level ``\$1''" ),
+ 'toofewexpiries' => array( 'code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed" ),
+ 'cantimport' => array( 'code' => 'cantimport', 'info' => "You don't have permission to import pages" ),
+ 'cantimport-upload' => array( 'code' => 'cantimport-upload', 'info' => "You don't have permission to import uploaded pages" ),
+ 'nouploadmodule' => array( 'code' => 'nomodule', 'info' => 'No upload module set' ),
+ 'importnofile' => array( 'code' => 'nofile', 'info' => "You didn't upload a file" ),
+ 'importuploaderrorsize' => array( 'code' => 'filetoobig', 'info' => 'The file you uploaded is bigger than the maximum upload size' ),
+ 'importuploaderrorpartial' => array( 'code' => 'partialupload', 'info' => 'The file was only partially uploaded' ),
+ 'importuploaderrortemp' => array( 'code' => 'notempdir', 'info' => 'The temporary upload directory is missing' ),
+ 'importcantopen' => array( 'code' => 'cantopenfile', 'info' => "Couldn't open the uploaded file" ),
+ 'import-noarticle' => array( 'code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified' ),
+ 'importbadinterwiki' => array( 'code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified' ),
+ 'import-unknownerror' => array( 'code' => 'import-unknownerror', 'info' => "Unknown error on import: ``\$1''" ),
+ 'cantoverwrite-sharedfile' => array( 'code' => 'cantoverwrite-sharedfile', 'info' => 'The target file exists on a shared repository and you do not have permission to override it' ),
+ 'sharedfile-exists' => array( 'code' => 'fileexists-sharedrepo-perm', 'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.' ),
+ 'mustbeposted' => array( 'code' => 'mustbeposted', 'info' => "The \$1 module requires a POST request" ),
+ 'show' => array( 'code' => 'show', 'info' => 'Incorrect parameter - mutually exclusive values may not be supplied' ),
// 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 kilobytes"),
- '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, prependtext and undo parameters must be set"),
- 'emptynewsection' => array('code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.'),
- 'revwrongpage' => array('code' => 'revwrongpage', 'info' => "r\$1 is not a revision of ``\$2''"),
- 'undo-failure' => array('code' => 'undofailure', 'info' => 'Undo failed due to conflicting intermediate edits'),
+ '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 kilobytes" ),
+ '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, prependtext and undo parameters must be set" ),
+ 'emptynewsection' => array( 'code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.' ),
+ 'revwrongpage' => array( 'code' => 'revwrongpage', 'info' => "r\$1 is not a revision of ``\$2''" ),
+ 'undo-failure' => array( 'code' => 'undofailure', 'info' => 'Undo failed due to conflicting intermediate edits' ),
+
+ // uploadMsgs
+ 'invalid-session-key' => array( 'code' => 'invalid-session-key', 'info' => 'Not a valid session key' ),
+ 'nouploadmodule' => array( 'code' => 'nouploadmodule', 'info' => 'No upload module set' ),
+ 'uploaddisabled' => array( 'code' => 'uploaddisabled', 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' ),
);
/**
+ * Helper function for readonly errors
+ */
+ public function dieReadOnly() {
+ $parsed = $this->parseMsg( array( 'readonlytext' ) );
+ $this->dieUsage( $parsed['info'], $parsed['code'], /* http error */ 0,
+ array( 'readonlyreason' => wfReadOnlyReason() ) );
+ }
+
+ /**
* Output the error message related to a certain array
* @param $error array Element of a getUserPermissionsErrors()-style array
*/
- public function dieUsageMsg($error) {
- $parsed = $this->parseMsg($error);
- $this->dieUsage($parsed['info'], $parsed['code']);
+ public function dieUsageMsg( $error ) {
+ $parsed = $this->parseMsg( $error );
+ $this->dieUsage( $parsed['info'], $parsed['code'] );
}
-
+
/**
* Return the error message related to a certain array
* @param $error array Element of a getUserPermissionsErrors()-style array
* @return array('code' => code, 'info' => info)
*/
- public function parseMsg($error) {
- $key = array_shift($error);
- if(isset(self::$messageMap[$key]))
- return array( 'code' =>
- wfMsgReplaceArgs(self::$messageMap[$key]['code'], $error),
+ public function parseMsg( $error ) {
+ $key = array_shift( $error );
+ if ( isset( self::$messageMap[$key] ) ) {
+ return array( 'code' =>
+ wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ),
'info' =>
- wfMsgReplaceArgs(self::$messageMap[$key]['info'], $error)
+ wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error )
);
+ }
// If the key isn't present, throw an "unknown error"
- return $this->parseMsg(array('unknownerror', $key));
+ return $this->parseMsg( array( 'unknownerror', $key ) );
}
/**
@@ -852,8 +970,8 @@ abstract class ApiBase {
* @param $method string Method or function name
* @param $message string Error message
*/
- protected static function dieDebug($method, $message) {
- wfDebugDieBacktrace("Internal error in $method: $message");
+ protected static function dieDebug( $method, $message ) {
+ wfDebugDieBacktrace( "Internal error in $method: $message" );
}
/**
@@ -887,6 +1005,59 @@ abstract class ApiBase {
return false;
}
+ /**
+ * Returns the token salt if there is one, '' if the module doesn't require a salt, else false if the module doesn't need a token
+ * @returns bool
+ */
+ public function getTokenSalt() {
+ return false;
+ }
+
+ /**
+ * Returns a list of all possible errors returned by the module
+ * @return array in the format of array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... )
+ */
+ public function getPossibleErrors() {
+ $ret = array();
+
+ if ( $this->mustBePosted() ) {
+ $ret[] = array( 'mustbeposted', $this->getModuleName() );
+ }
+
+ if ( $this->isReadMode() ) {
+ $ret[] = array( 'readrequired' );
+ }
+
+ if ( $this->isWriteMode() ) {
+ $ret[] = array( 'writerequired' );
+ $ret[] = array( 'writedisabled' );
+ }
+
+ if ( $this->getTokenSalt() !== false ) {
+ $ret[] = array( 'missingparam', 'token' );
+ $ret[] = array( 'sessionfailure' );
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Parses a list of errors into a standardised format
+ * @param $errors array List of errors. Items can be in the for array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... )
+ * @return array Parsed list of errors with items in the form array( 'code' => ..., 'info' => ... )
+ */
+ public function parseErrors( $errors ) {
+ $ret = array();
+
+ foreach ( $errors as $row ) {
+ if ( isset( $row['code'] ) && isset( $row['info'] ) ) {
+ $ret[] = $row;
+ } else {
+ $ret[] = $this->parseMsg( $row );
+ }
+ }
+ return $ret;
+ }
/**
* Profiling: total module execution time
@@ -897,24 +1068,27 @@ abstract class ApiBase {
* Start module profiling
*/
public function profileIn() {
- if ($this->mTimeIn !== 0)
- ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileOut()');
- $this->mTimeIn = microtime(true);
- wfProfileIn($this->getModuleProfileName());
+ if ( $this->mTimeIn !== 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'called twice without calling profileOut()' );
+ }
+ $this->mTimeIn = microtime( true );
+ wfProfileIn( $this->getModuleProfileName() );
}
/**
* End module profiling
*/
public function profileOut() {
- if ($this->mTimeIn === 0)
- ApiBase :: dieDebug(__METHOD__, 'called without calling profileIn() first');
- if ($this->mDBTimeIn !== 0)
- ApiBase :: dieDebug(__METHOD__, 'must be called after database profiling is done with profileDBOut()');
+ if ( $this->mTimeIn === 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'called without calling profileIn() first' );
+ }
+ if ( $this->mDBTimeIn !== 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'must be called after database profiling is done with profileDBOut()' );
+ }
- $this->mModuleTime += microtime(true) - $this->mTimeIn;
+ $this->mModuleTime += microtime( true ) - $this->mTimeIn;
$this->mTimeIn = 0;
- wfProfileOut($this->getModuleProfileName());
+ wfProfileOut( $this->getModuleProfileName() );
}
/**
@@ -922,9 +1096,10 @@ abstract class ApiBase {
* of the profiling state the module was in. This method does such cleanup.
*/
public function safeProfileOut() {
- if ($this->mTimeIn !== 0) {
- if ($this->mDBTimeIn !== 0)
+ if ( $this->mTimeIn !== 0 ) {
+ if ( $this->mDBTimeIn !== 0 ) {
$this->profileDBOut();
+ }
$this->profileOut();
}
}
@@ -934,8 +1109,9 @@ abstract class ApiBase {
* @return float
*/
public function getProfileTime() {
- if ($this->mTimeIn !== 0)
- ApiBase :: dieDebug(__METHOD__, 'called without calling profileOut() first');
+ if ( $this->mTimeIn !== 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'called without calling profileOut() first' );
+ }
return $this->mModuleTime;
}
@@ -948,29 +1124,33 @@ abstract class ApiBase {
* Start module profiling
*/
public function profileDBIn() {
- if ($this->mTimeIn === 0)
- ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()');
- if ($this->mDBTimeIn !== 0)
- ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileDBOut()');
- $this->mDBTimeIn = microtime(true);
- wfProfileIn($this->getModuleProfileName(true));
+ if ( $this->mTimeIn === 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'must be called while profiling the entire module with profileIn()' );
+ }
+ if ( $this->mDBTimeIn !== 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'called twice without calling profileDBOut()' );
+ }
+ $this->mDBTimeIn = microtime( true );
+ wfProfileIn( $this->getModuleProfileName( true ) );
}
/**
* End database profiling
*/
public function profileDBOut() {
- if ($this->mTimeIn === 0)
- ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()');
- if ($this->mDBTimeIn === 0)
- ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBIn() first');
+ if ( $this->mTimeIn === 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'must be called while profiling the entire module with profileIn()' );
+ }
+ if ( $this->mDBTimeIn === 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'called without calling profileDBIn() first' );
+ }
- $time = microtime(true) - $this->mDBTimeIn;
+ $time = microtime( true ) - $this->mDBTimeIn;
$this->mDBTimeIn = 0;
$this->mDBTime += $time;
$this->getMain()->mDBTime += $time;
- wfProfileOut($this->getModuleProfileName(true));
+ wfProfileOut( $this->getModuleProfileName( true ) );
}
/**
@@ -978,8 +1158,9 @@ abstract class ApiBase {
* @return float
*/
public function getProfileDBTime() {
- if ($this->mDBTimeIn !== 0)
- ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first');
+ if ( $this->mDBTimeIn !== 0 ) {
+ ApiBase::dieDebug( __METHOD__, 'called without calling profileDBOut() first' );
+ }
return $this->mDBTime;
}
@@ -989,20 +1170,20 @@ abstract class ApiBase {
* @param $name string Description of the printed value
* @param $backtrace bool If true, print a backtrace
*/
- public static function debugPrint($value, $name = 'unknown', $backtrace = false) {
+ public static function debugPrint( $value, $name = 'unknown', $backtrace = false ) {
print "\n\n<pre><b>Debugging value '$name':</b>\n\n";
- var_export($value);
- if ($backtrace)
+ var_export( $value );
+ if ( $backtrace ) {
print "\n" . wfBacktrace();
+ }
print "\n</pre>\n";
}
-
/**
* Returns a string that identifies the version of this class.
* @return string
*/
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiBase.php 50217 2009-05-05 13:12:16Z tstarling $';
+ return __CLASS__ . ': $Id: ApiBase.php 70066 2010-07-28 05:52:32Z tstarling $';
}
}
diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php
index 1c0bd5ac..91bbaf6d 100644
--- a/includes/api/ApiBlock.php
+++ b/includes/api/ApiBlock.php
@@ -1,10 +1,10 @@
<?php
-/*
+/**
* Created on Sep 4, 2007
* API for MediaWiki 1.8+
*
- * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once( "ApiBase.php" );
}
/**
@@ -38,8 +38,8 @@ class ApiBlock extends ApiBase {
/**
* Std ctor.
*/
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
}
/**
@@ -52,31 +52,30 @@ class ApiBlock extends ApiBase {
global $wgUser, $wgBlockAllowsUTEdit;
$params = $this->extractRequestParams();
- if($params['gettoken'])
- {
+ if ( $params['gettoken'] ) {
$res['blocktoken'] = $wgUser->editToken();
- $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
return;
}
- if(is_null($params['user']))
- $this->dieUsageMsg(array('missingparam', 'user'));
- if(is_null($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
- if(!$wgUser->isAllowed('block'))
- $this->dieUsageMsg(array('cantblock'));
- if($params['hidename'] && !$wgUser->isAllowed('hideuser'))
- $this->dieUsageMsg(array('canthide'));
- if($params['noemail'] && !$wgUser->isAllowed('blockemail'))
- $this->dieUsageMsg(array('cantblock-email'));
-
- $form = new IPBlockForm('');
+ if ( is_null( $params['user'] ) ) {
+ $this->dieUsageMsg( array( 'missingparam', 'user' ) );
+ }
+ if ( !$wgUser->isAllowed( 'block' ) ) {
+ $this->dieUsageMsg( array( 'cantblock' ) );
+ }
+ if ( $params['hidename'] && !$wgUser->isAllowed( 'hideuser' ) ) {
+ $this->dieUsageMsg( array( 'canthide' ) );
+ }
+ if ( $params['noemail'] && !IPBlockForm::canBlockEmail( $wgUser ) ) {
+ $this->dieUsageMsg( array( 'cantblock-email' ) );
+ }
+
+ $form = new IPBlockForm( '' );
$form->BlockAddress = $params['user'];
- $form->BlockReason = (is_null($params['reason']) ? '' : $params['reason']);
+ $form->BlockReason = ( is_null( $params['reason'] ) ? '' : $params['reason'] );
$form->BlockReasonList = 'other';
- $form->BlockExpiry = ($params['expiry'] == 'never' ? 'infinite' : $params['expiry']);
+ $form->BlockExpiry = ( $params['expiry'] == 'never' ? 'infinite' : $params['expiry'] );
$form->BlockOther = '';
$form->BlockAnonOnly = $params['anononly'];
$form->BlockCreateAccount = $params['nocreate'];
@@ -87,39 +86,48 @@ class ApiBlock extends ApiBase {
$form->BlockReblock = $params['reblock'];
$userID = $expiry = null;
- $retval = $form->doBlock($userID, $expiry);
- if(count($retval))
+ $retval = $form->doBlock( $userID, $expiry );
+ if ( count( $retval ) ) {
// We don't care about multiple errors, just report one of them
- $this->dieUsageMsg($retval);
+ $this->dieUsageMsg( $retval );
+ }
$res['user'] = $params['user'];
- $res['userID'] = intval($userID);
- $res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : wfTimestamp(TS_ISO_8601, $expiry));
+ $res['userID'] = intval( $userID );
+ $res['expiry'] = ( $expiry == Block::infinity() ? 'infinite' : wfTimestamp( TS_ISO_8601, $expiry ) );
$res['reason'] = $params['reason'];
- if($params['anononly'])
+ if ( $params['anononly'] ) {
$res['anononly'] = '';
- if($params['nocreate'])
+ }
+ if ( $params['nocreate'] ) {
$res['nocreate'] = '';
- if($params['autoblock'])
+ }
+ if ( $params['autoblock'] ) {
$res['autoblock'] = '';
- if($params['noemail'])
+ }
+ if ( $params['noemail'] ) {
$res['noemail'] = '';
- if($params['hidename'])
+ }
+ if ( $params['hidename'] ) {
$res['hidename'] = '';
- if($params['allowusertalk'])
+ }
+ if ( $params['allowusertalk'] ) {
$res['allowusertalk'] = '';
+ }
- $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
}
public function getAllowedParams() {
- return array (
+ return array(
'user' => null,
'token' => null,
'gettoken' => false,
@@ -136,7 +144,7 @@ class ApiBlock extends ApiBase {
}
public function getParamDescription() {
- return array (
+ return array(
'user' => 'Username, IP address or IP range you want to block',
'token' => 'A block token previously obtained through the gettoken parameter or prop=info',
'gettoken' => 'If set, a block token will be returned, and no other action will be taken',
@@ -157,15 +165,28 @@ class ApiBlock extends ApiBase {
'Block a user.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'user' ),
+ array( 'cantblock' ),
+ array( 'canthide' ),
+ array( 'cantblock-email' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
- return array (
+ return array(
'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike',
'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate&autoblock&noemail'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiBlock.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiBlock.php 62766 2010-02-21 12:32:46Z ashley $';
}
}
diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php
index 9431ad78..2b349bd7 100644
--- a/includes/api/ApiDelete.php
+++ b/includes/api/ApiDelete.php
@@ -1,10 +1,10 @@
<?php
-/*
+/**
* Created on Jun 30, 2007
* API for MediaWiki 1.8+
*
- * Copyright (C) 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@home.nl
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,12 +22,11 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once( "ApiBase.php" );
}
-
/**
* API module that facilitates deleting pages. The API eqivalent of action=delete.
* Requires API write mode to be enabled.
@@ -36,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiDelete extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
}
/**
@@ -49,65 +48,60 @@ class ApiDelete extends ApiBase {
*/
public function execute() {
global $wgUser;
+
$params = $this->extractRequestParams();
- $this->requireOnlyOneParameter($params, 'title', 'pageid');
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
+ $this->requireOnlyOneParameter( $params, 'title', 'pageid' );
- if(isset($params['title']))
- {
- $titleObj = Title::newFromText($params['title']);
- if(!$titleObj)
- $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ if ( isset( $params['title'] ) ) {
+ $titleObj = Title::newFromText( $params['title'] );
+ if ( !$titleObj ) {
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
+ }
+ } elseif ( isset( $params['pageid'] ) ) {
+ $titleObj = Title::newFromID( $params['pageid'] );
+ if ( !$titleObj ) {
+ $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) );
+ }
}
- else if(isset($params['pageid']))
- {
- $titleObj = Title::newFromID($params['pageid']);
- if(!$titleObj)
- $this->dieUsageMsg(array('nosuchpageid', $params['pageid']));
+ if ( !$titleObj->exists() ) {
+ $this->dieUsageMsg( array( 'notanarticle' ) );
}
- if(!$titleObj->exists())
- $this->dieUsageMsg(array('notanarticle'));
-
- $reason = (isset($params['reason']) ? $params['reason'] : NULL);
- if ($titleObj->getNamespace() == NS_FILE) {
- $retval = self::deleteFile($params['token'], $titleObj, $params['oldimage'], $reason, false);
- if(count($retval))
- // We don't care about multiple errors, just report one of them
- $this->dieUsageMsg(reset($retval));
+
+ $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
+ if ( $titleObj->getNamespace() == NS_FILE ) {
+ $retval = self::deleteFile( $params['token'], $titleObj, $params['oldimage'], $reason, false );
+ if ( count( $retval ) ) {
+ $this->dieUsageMsg( reset( $retval ) ); // We don't care about multiple errors, just report one of them
+ }
} else {
- $articleObj = new Article($titleObj);
- if($articleObj->isBigDeletion() && !$wgUser->isAllowed('bigdelete')) {
- global $wgDeleteRevisionsLimit;
- $this->dieUsageMsg(array('delete-toobig', $wgDeleteRevisionsLimit));
+ $articleObj = new Article( $titleObj );
+ $retval = self::delete( $articleObj, $params['token'], $reason );
+
+ if ( count( $retval ) ) {
+ $this->dieUsageMsg( reset( $retval ) ); // We don't care about multiple errors, just report one of them
}
- $retval = self::delete($articleObj, $params['token'], $reason);
-
- if(count($retval))
- // We don't care about multiple errors, just report one of them
- $this->dieUsageMsg(reset($retval));
-
- if($params['watch'] || $wgUser->getOption('watchdeletion'))
+
+ if ( $params['watch'] || $wgUser->getOption( 'watchdeletion' ) ) {
$articleObj->doWatch();
- else if($params['unwatch'])
+ } elseif ( $params['unwatch'] ) {
$articleObj->doUnwatch();
+ }
}
- $r = array('title' => $titleObj->getPrefixedText(), 'reason' => $reason);
- $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ $r = array( 'title' => $titleObj->getPrefixedText(), 'reason' => $reason );
+ $this->getResult()->addValue( null, $this->getModuleName(), $r );
}
- private static function getPermissionsError(&$title, $token) {
+ private static function getPermissionsError( &$title, $token ) {
global $wgUser;
-
+
// Check permissions
- $errors = $title->getUserPermissionsErrors('delete', $wgUser);
- if (count($errors) > 0) return $errors;
-
- // Check token
- if(!$wgUser->matchEditToken($token))
- return array(array('sessionfailure'));
+ $errors = $title->getUserPermissionsErrors( 'delete', $wgUser );
+ if ( count( $errors ) > 0 ) {
+ return $errors;
+ }
+
return array();
}
@@ -119,70 +113,84 @@ class ApiDelete extends ApiBase {
* @param string $reason - Reason for the deletion. Autogenerated if NULL
* @return Title::getUserPermissionsErrors()-like array
*/
- public static function delete(&$article, $token, &$reason = NULL)
- {
+ public static function delete( &$article, $token, &$reason = null ) {
global $wgUser;
+ if ( $article->isBigDeletion() && !$wgUser->isAllowed( 'bigdelete' ) ) {
+ global $wgDeleteRevisionsLimit;
+ return array( array( 'delete-toobig', $wgDeleteRevisionsLimit ) );
+ }
$title = $article->getTitle();
- $errors = self::getPermissionsError($title, $token);
- if (count($errors)) return $errors;
+ $errors = self::getPermissionsError( $title, $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
+ 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'));
+ $reason = $article->generateReason( $hasHistory );
+ if ( $reason === false ) {
+ return array( array( 'cannotdelete' ) );
+ }
}
$error = '';
- if (!wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason, $error)))
- $this->dieUsageMsg(array('hookaborted', $error));
+ if ( !wfRunHooks( 'ArticleDelete', array( &$article, &$wgUser, &$reason, $error ) ) ) {
+ $this->dieUsageMsg( array( 'hookaborted', $error ) );
+ }
// Luckily, Article.php provides a reusable delete function that does the hard work for us
- if($article->doDeleteArticle($reason)) {
- wfRunHooks('ArticleDeleteComplete', array(&$article, &$wgUser, $reason, $article->getId()));
+ if ( $article->doDeleteArticle( $reason ) ) {
+ wfRunHooks( 'ArticleDeleteComplete', array( &$article, &$wgUser, $reason, $article->getId() ) );
return array();
}
- return array(array('cannotdelete', $article->mTitle->getPrefixedText()));
+ 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;
+ 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'));
+ if ( $oldimage && !FileDeleteForm::isValidOldSpec( $oldimage ) ) {
+ return array( array( 'invalidoldimage' ) );
+ }
- $file = wfFindFile($title, false, FileRepo::FIND_IGNORE_REDIRECT);
+ $file = wfFindFile( $title, array( 'ignoreRedirect' => true ) );
$oldfile = false;
-
- if( $oldimage )
+
+ if ( $oldimage ) {
$oldfile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $oldimage );
-
- if( !FileDeleteForm::haveDeletableFile($file, $oldfile, $oldimage) )
- return array(array('nofile'));
- if (is_null($reason)) # Log and RC don't like null reasons
+ }
+
+ if ( !FileDeleteForm::haveDeletableFile( $file, $oldfile, $oldimage ) ) {
+ return self::delete( new Article( $title ), $token, $reason );
+ }
+ if ( is_null( $reason ) ) { // Log and RC don't like null reasons
$reason = '';
+ }
$status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
-
- if( !$status->isGood() )
- return array(array('cannotdelete', $title->getPrefixedText()));
-
+
+ if ( !$status->isGood() ) {
+ return array( array( 'cannotdelete', $title->getPrefixedText() ) );
+ }
+
return array();
}
-
- public function mustBePosted() { return true; }
+
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
}
public function getAllowedParams() {
- return array (
+ return array(
'title' => null,
'pageid' => array(
ApiBase::PARAM_TYPE => 'integer'
@@ -196,7 +204,7 @@ class ApiDelete extends ApiBase {
}
public function getParamDescription() {
- return array (
+ return array(
'title' => 'Title of the page you want to delete. Cannot be used together with pageid',
'pageid' => 'Page ID of the page you want to delete. Cannot be used together with title',
'token' => 'A delete token previously retrieved through prop=info',
@@ -213,14 +221,27 @@ class ApiDelete extends ApiBase {
);
}
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'invalidtitle', 'title' ),
+ array( 'nosuchpageid', 'pageid' ),
+ array( 'notanarticle' ),
+ array( 'hookaborted', 'error' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
+
protected function getExamples() {
- return array (
+ return array(
'api.php?action=delete&title=Main%20Page&token=123ABC',
'api.php?action=delete&title=Main%20Page&token=123ABC&reason=Preparing%20for%20move'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiDelete.php 48122 2009-03-07 12:58:41Z catrope $';
+ return __CLASS__ . ': $Id: ApiDelete.php 62703 2010-02-19 12:54:09Z ashley $';
}
-}
+} \ No newline at end of file
diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php
index 9e0bf56e..60e0e7ee 100644
--- a/includes/api/ApiDisabled.php
+++ b/includes/api/ApiDisabled.php
@@ -1,10 +1,10 @@
<?php
-/*
+/**
* Created on Sep 25, 2008
* API for MediaWiki 1.8+
*
- * Copyright (C) 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@home.nl
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,12 +22,11 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once( "ApiBase.php" );
}
-
/**
* API module that dies with an error immediately.
*
@@ -40,12 +39,12 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiDisabled extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
}
public function execute() {
- $this->dieUsage("The ``{$this->getModuleName()}'' module has been disabled.", 'moduledisabled');
+ $this->dieUsage( "The ``{$this->getModuleName()}'' module has been disabled.", 'moduledisabled' );
}
public function isReadMode() {
@@ -53,11 +52,11 @@ class ApiDisabled extends ApiBase {
}
public function getAllowedParams() {
- return array ();
+ return array();
}
public function getParamDescription() {
- return array ();
+ return array();
}
public function getDescription() {
@@ -67,10 +66,10 @@ class ApiDisabled extends ApiBase {
}
protected function getExamples() {
- return array ();
+ return array();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiDisabled.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiDisabled.php 62783 2010-02-21 18:09:00Z ashley $';
}
}
diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php
index d4a57b83..50a9836a 100644
--- a/includes/api/ApiEditPage.php
+++ b/includes/api/ApiEditPage.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -37,242 +37,295 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiEditPage extends ApiBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName);
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName );
}
public function execute() {
global $wgUser;
$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']) &&
- $params['undo'] == 0)
- $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 ( is_null( $params['title'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'title' ) );
+
+ if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) &&
+ is_null( $params['prependtext'] ) &&
+ $params['undo'] == 0 )
+ $this->dieUsageMsg( array( 'missingtext' ) );
+
+ $titleObj = Title::newFromText( $params['title'] );
+ if ( !$titleObj || $titleObj->isExternal() )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
+
// Some functions depend on $wgTitle == $ep->mTitle
global $wgTitle;
$wgTitle = $titleObj;
- if($params['createonly'] && $titleObj->exists())
- $this->dieUsageMsg(array('createonly-exists'));
- if($params['nocreate'] && !$titleObj->exists())
- $this->dieUsageMsg(array('nocreate-missing'));
+ 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(count($errors))
- $this->dieUsageMsg($errors[0]);
+ $errors = $titleObj->getUserPermissionsErrors( 'edit', $wgUser );
+ if ( !$titleObj->exists() )
+ $errors = array_merge( $errors, $titleObj->getUserPermissionsErrors( 'create', $wgUser ) );
+ if ( count( $errors ) )
+ $this->dieUsageMsg( $errors[0] );
- $articleObj = new Article($titleObj);
+ $articleObj = new Article( $titleObj );
$toMD5 = $params['text'];
- if(!is_null($params['appendtext']) || !is_null($params['prependtext']))
+ if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) )
{
// For non-existent pages, Article::getContent()
// returns an interface message rather than ''
// We do want getContent()'s behavior for non-existent
// MediaWiki: pages, though
- if($articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI)
+ if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI )
$content = '';
else
$content = $articleObj->getContent();
+
+ if ( !is_null( $params['section'] ) )
+ {
+ // Process the content for section edits
+ global $wgParser;
+ $section = intval( $params['section'] );
+ $content = $wgParser->getSection( $content, $section, false );
+ if ( $content === false )
+ $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
+ }
$params['text'] = $params['prependtext'] . $content . $params['appendtext'];
$toMD5 = $params['prependtext'] . $params['appendtext'];
}
- if($params['undo'] > 0)
+ if ( $params['undo'] > 0 )
{
- if($params['undoafter'] > 0)
+ if ( $params['undoafter'] > 0 )
{
- if($params['undo'] < $params['undoafter'])
- list($params['undo'], $params['undoafter']) =
- array($params['undoafter'], $params['undo']);
- $undoafterRev = Revision::newFromID($params['undoafter']);
+ if ( $params['undo'] < $params['undoafter'] )
+ list( $params['undo'], $params['undoafter'] ) =
+ array( $params['undoafter'], $params['undo'] );
+ $undoafterRev = Revision::newFromID( $params['undoafter'] );
}
- $undoRev = Revision::newFromID($params['undo']);
- if(is_null($undoRev) || $undoRev->isDeleted(Revision::DELETED_TEXT))
- $this->dieUsageMsg(array('nosuchrevid', $params['undo']));
- if($params['undoafter'] == 0)
+ $undoRev = Revision::newFromID( $params['undo'] );
+ if ( is_null( $undoRev ) || $undoRev->isDeleted( Revision::DELETED_TEXT ) )
+ $this->dieUsageMsg( array( 'nosuchrevid', $params['undo'] ) );
+
+ if ( $params['undoafter'] == 0 )
$undoafterRev = $undoRev->getPrevious();
- if(is_null($undoafterRev) || $undoafterRev->isDeleted(Revision::DELETED_TEXT))
- $this->dieUsageMsg(array('nosuchrevid', $params['undoafter']));
- if($undoRev->getPage() != $articleObj->getID())
- $this->dieUsageMsg(array('revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText()));
- if($undoafterRev->getPage() != $articleObj->getID())
- $this->dieUsageMsg(array('revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText()));
- $newtext = $articleObj->getUndoText($undoRev, $undoafterRev);
- if($newtext === false)
- $this->dieUsageMsg(array('undo-failure'));
+ if ( is_null( $undoafterRev ) || $undoafterRev->isDeleted( Revision::DELETED_TEXT ) )
+ $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) );
+
+ if ( $undoRev->getPage() != $articleObj->getID() )
+ $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) );
+ if ( $undoafterRev->getPage() != $articleObj->getID() )
+ $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) );
+
+ $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev );
+ if ( $newtext === false )
+ $this->dieUsageMsg( array( 'undo-failure' ) );
$params['text'] = $newtext;
// If no summary was given and we only undid one rev,
// use an autosummary
- if(is_null($params['summary']) && $titleObj->getNextRevisionID($undoafterRev->getID()) == $params['undo'])
- $params['summary'] = wfMsgForContent('undo-summary', $params['undo'], $undoRev->getUserText());
+ if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] )
+ $params['summary'] = wfMsgForContent( 'undo-summary', $params['undo'], $undoRev->getUserText() );
}
- # See if the MD5 hash checks out
- if(!is_null($params['md5']))
- if(md5($toMD5) !== $params['md5'])
- $this->dieUsageMsg(array('hashcheckfailed'));
+ // See if the MD5 hash checks out
+ if ( !is_null( $params['md5'] ) && md5( $toMD5 ) !== $params['md5'] )
+ $this->dieUsageMsg( array( 'hashcheckfailed' ) );
- $ep = new EditPage($articleObj);
+ $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'],
+ $reqArr = array( 'wpTextbox1' => $params['text'],
+ 'wpEditToken' => $params['token'],
'wpIgnoreBlankSummary' => ''
);
- if(!is_null($params['summary']))
+
+ 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']);
+
+ // 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();
- if(!is_null($params['starttimestamp']) && $params['starttimestamp'] != '')
- $reqArr['wpStarttime'] = wfTimestamp(TS_MW, $params['starttimestamp']);
+
+ if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' )
+ $reqArr['wpStarttime'] = wfTimestamp( TS_MW, $params['starttimestamp'] );
else
- # Fake wpStartime
- $reqArr['wpStarttime'] = $reqArr['wpEdittime'];
- if($params['minor'] || (!$params['notminor'] && $wgUser->getOption('minordefault')))
+ $reqArr['wpStarttime'] = $reqArr['wpEdittime']; // Fake wpStartime
+
+ if ( $params['minor'] || ( !$params['notminor'] && $wgUser->getOption( 'minordefault' ) ) )
$reqArr['wpMinoredit'] = '';
- if($params['recreate'])
+
+ if ( $params['recreate'] )
$reqArr['wpRecreate'] = '';
- if(!is_null($params['section']))
+
+ 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");
+ $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'];
}
else
$reqArr['wpSection'] = '';
- 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())
+ // Handle watchlist settings
+ switch ( $params['watchlist'] )
+ {
+ case 'watch':
+ $watch = true;
+ break;
+ case 'unwatch':
+ $watch = false;
+ break;
+ case 'preferences':
+ if ( $titleObj->exists() )
+ $watch = $wgUser->getOption( 'watchdefault' ) || $titleObj->userIsWatching();
+ else
+ $watch = $wgUser->getOption( 'watchcreations' );
+ break;
+ case 'nochange':
+ default:
+ $watch = $titleObj->userIsWatching();
+ }
+ // Deprecated parameters
+ if ( $params['watch'] )
$watch = true;
- else
+ elseif ( $params['unwatch'] )
$watch = false;
- if($watch)
+
+ if ( $watch )
$reqArr['wpWatchthis'] = '';
- $req = new FauxRequest($reqArr, true);
- $ep->importFormData($req);
+ $req = new FauxRequest( $reqArr, true );
+ $ep->importFormData( $req );
- # Run hooks
- # Handle CAPTCHA parameters
+ // Run hooks
+ // Handle CAPTCHA parameters
global $wgRequest;
- if(!is_null($params['captchaid']))
+ if ( !is_null( $params['captchaid'] ) )
$wgRequest->setVal( 'wpCaptchaId', $params['captchaid'] );
- if(!is_null($params['captchaword']))
+ if ( !is_null( $params['captchaword'] ) )
$wgRequest->setVal( 'wpCaptchaWord', $params['captchaword'] );
+
$r = array();
- if(!wfRunHooks('APIEditBeforeSave', array(&$ep, $ep->textbox1, &$r)))
+ if ( !wfRunHooks( 'APIEditBeforeSave', array( $ep, $ep->textbox1, &$r ) ) )
{
- if(count($r))
+ if ( count( $r ) )
{
$r['result'] = "Failure";
- $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ $this->getResult()->addValue( null, $this->getModuleName(), $r );
return;
}
else
- $this->dieUsageMsg(array('hookaborted'));
+ $this->dieUsageMsg( array( 'hookaborted' ) );
}
- # Do the actual save
+ // Do the actual save
$oldRevId = $articleObj->getRevIdFetched();
$result = null;
- # Fake $wgRequest for some hooks inside EditPage
- # FIXME: This interface SUCKS
+ // Fake $wgRequest for some hooks inside EditPage
+ // FIXME: This interface SUCKS
$oldRequest = $wgRequest;
$wgRequest = $req;
- $retval = $ep->internalAttemptSave($result, $wgUser->isAllowed('bot') && $params['bot']);
+ $retval = $ep->internalAttemptSave( $result, $wgUser->isAllowed( 'bot' ) && $params['bot'] );
$wgRequest = $oldRequest;
- switch($retval)
+ switch( $retval )
{
case EditPage::AS_HOOK_ERROR:
case EditPage::AS_HOOK_ERROR_EXPECTED:
- $this->dieUsageMsg(array('hookaborted'));
+ $this->dieUsageMsg( array( 'hookaborted' ) );
+
case EditPage::AS_IMAGE_REDIRECT_ANON:
- $this->dieUsageMsg(array('noimageredirect-anon'));
+ $this->dieUsageMsg( array( 'noimageredirect-anon' ) );
+
case EditPage::AS_IMAGE_REDIRECT_LOGGED:
- $this->dieUsageMsg(array('noimageredirect-logged'));
+ $this->dieUsageMsg( array( 'noimageredirect-logged' ) );
+
case EditPage::AS_SPAM_ERROR:
- $this->dieUsageMsg(array('spamdetected', $result['spam']));
+ $this->dieUsageMsg( array( 'spamdetected', $result['spam'] ) );
+
case EditPage::AS_FILTERING:
- $this->dieUsageMsg(array('filtered'));
+ $this->dieUsageMsg( array( 'filtered' ) );
+
case EditPage::AS_BLOCKED_PAGE_FOR_USER:
- $this->dieUsageMsg(array('blockedtext'));
+ $this->dieUsageMsg( array( 'blockedtext' ) );
+
case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
case EditPage::AS_CONTENT_TOO_BIG:
global $wgMaxArticleSize;
- $this->dieUsageMsg(array('contenttoobig', $wgMaxArticleSize));
+ $this->dieUsageMsg( array( 'contenttoobig', $wgMaxArticleSize ) );
+
case EditPage::AS_READ_ONLY_PAGE_ANON:
- $this->dieUsageMsg(array('noedit-anon'));
+ $this->dieUsageMsg( array( 'noedit-anon' ) );
+
case EditPage::AS_READ_ONLY_PAGE_LOGGED:
- $this->dieUsageMsg(array('noedit'));
+ $this->dieUsageMsg( array( 'noedit' ) );
+
case EditPage::AS_READ_ONLY_PAGE:
- $this->dieUsageMsg(array('readonlytext'));
+ $this->dieReadOnly();
+
case EditPage::AS_RATE_LIMITED:
- $this->dieUsageMsg(array('actionthrottledtext'));
+ $this->dieUsageMsg( array( 'actionthrottledtext' ) );
+
case EditPage::AS_ARTICLE_WAS_DELETED:
- $this->dieUsageMsg(array('wasdeleted'));
+ $this->dieUsageMsg( array( 'wasdeleted' ) );
+
case EditPage::AS_NO_CREATE_PERMISSION:
- $this->dieUsageMsg(array('nocreate-loggedin'));
+ $this->dieUsageMsg( array( 'nocreate-loggedin' ) );
+
case EditPage::AS_BLANK_ARTICLE:
- $this->dieUsageMsg(array('blankpage'));
+ $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
+ $this->dieUsageMsg( array( 'editconflict' ) );
+
+ // case EditPage::AS_SUMMARY_NEEDED: Can't happen since we set wpIgnoreBlankSummary
case EditPage::AS_TEXTBOX_EMPTY:
- $this->dieUsageMsg(array('emptynewsection'));
- case EditPage::AS_END:
- # This usually means some kind of race condition
- # or DB weirdness occurred. Throw an unknown error here.
- $this->dieUsageMsg(array('unknownerror'));
+ $this->dieUsageMsg( array( 'emptynewsection' ) );
+
case EditPage::AS_SUCCESS_NEW_ARTICLE:
$r['new'] = '';
case EditPage::AS_SUCCESS_UPDATE:
$r['result'] = "Success";
- $r['pageid'] = intval($titleObj->getArticleID());
+ $r['pageid'] = intval( $titleObj->getArticleID() );
$r['title'] = $titleObj->getPrefixedText();
- # HACK: We create a new Article object here because getRevIdFetched()
- # refuses to be run twice, and because Title::getLatestRevId()
- # won't fetch from the master unless we select for update, which we
- # don't want to do.
- $newArticle = new Article($titleObj);
+ // HACK: We create a new Article object here because getRevIdFetched()
+ // refuses to be run twice, and because Title::getLatestRevId()
+ // won't fetch from the master unless we select for update, which we
+ // don't want to do.
+ $newArticle = new Article( $titleObj );
$newRevId = $newArticle->getRevIdFetched();
- if($newRevId == $oldRevId)
+ if ( $newRevId == $oldRevId )
$r['nochange'] = '';
else
{
- $r['oldrevid'] = intval($oldRevId);
- $r['newrevid'] = intval($newRevId);
+ $r['oldrevid'] = intval( $oldRevId );
+ $r['newrevid'] = intval( $newRevId );
+ $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
+ $newArticle->getTimestamp() );
}
break;
+
+ case EditPage::AS_END:
+ // This usually means some kind of race condition
+ // or DB weirdness occurred. Fall through to throw an unknown
+ // error.
+
+ // This needs fixing higher up, as Article::doEdit should be
+ // used rather than Article::updateArticle, so that specific
+ // error conditions can be returned
default:
- $this->dieUsageMsg(array('unknownerror', $retval));
+ $this->dieUsageMsg( array( 'unknownerror', $retval ) );
}
- $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ $this->getResult()->addValue( null, $this->getModuleName(), $r );
}
public function mustBePosted() {
@@ -286,6 +339,41 @@ class ApiEditPage extends ApiBase {
protected function getDescription() {
return 'Create and edit pages.';
}
+
+ public function getPossibleErrors() {
+ global $wgMaxArticleSize;
+
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'title' ),
+ array( 'missingtext' ),
+ array( 'invalidtitle', 'title' ),
+ array( 'createonly-exists' ),
+ array( 'nocreate-missing' ),
+ array( 'nosuchrevid', 'undo' ),
+ array( 'nosuchrevid', 'undoafter' ),
+ array( 'revwrongpage', 'id', 'text' ),
+ array( 'undo-failure' ),
+ array( 'hashcheckfailed' ),
+ array( 'hookaborted' ),
+ array( 'noimageredirect-anon' ),
+ array( 'noimageredirect-logged' ),
+ array( 'spamdetected', 'spam' ),
+ array( 'filtered' ),
+ array( 'blockedtext' ),
+ array( 'contenttoobig', $wgMaxArticleSize ),
+ array( 'noedit-anon' ),
+ array( 'noedit' ),
+ array( 'actionthrottledtext' ),
+ array( 'wasdeleted' ),
+ array( 'nocreate-loggedin' ),
+ array( 'blankpage' ),
+ array( 'editconflict' ),
+ array( 'emptynewsection' ),
+ array( 'unknownerror', 'retval' ),
+ array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ),
+ array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ),
+ ) );
+ }
protected function getAllowedParams() {
return array (
@@ -304,8 +392,23 @@ class ApiEditPage extends ApiBase {
'nocreate' => false,
'captchaword' => null,
'captchaid' => null,
- 'watch' => false,
- 'unwatch' => false,
+ 'watch' => array(
+ ApiBase :: PARAM_DFLT => false,
+ ApiBase :: PARAM_DEPRECATED => true,
+ ),
+ 'unwatch' => array(
+ ApiBase :: PARAM_DFLT => false,
+ ApiBase :: PARAM_DEPRECATED => true,
+ ),
+ 'watchlist' => array(
+ ApiBase :: PARAM_DFLT => 'preferences',
+ ApiBase :: PARAM_TYPE => array(
+ 'watch',
+ 'unwatch',
+ 'preferences',
+ 'nochange'
+ ),
+ ),
'md5' => null,
'prependtext' => null,
'appendtext' => null,
@@ -328,10 +431,10 @@ class ApiEditPage extends ApiBase {
'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).',
+ 'basetimestamp' => array( 'Timestamp of the base revision (gotten through prop=revisions&rvprop=timestamp).',
'Used to detect edit conflicts; leave unset to ignore conflicts.'
),
- 'starttimestamp' => array('Timestamp when you obtained the edit token.',
+ 'starttimestamp' => array( 'Timestamp when you obtained the edit token.',
'Used to detect edit conflicts; leave unset to ignore conflicts.'
),
'recreate' => 'Override any errors about the article having been deleted in the meantime',
@@ -339,17 +442,21 @@ class ApiEditPage extends ApiBase {
'nocreate' => 'Throw an error if the page doesn\'t exist',
'watch' => 'Add the page to your watchlist',
'unwatch' => 'Remove the page from your watchlist',
+ 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch',
'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.'),
+ 'If set, the edit won\'t be done unless the hash is correct' ),
+ 'prependtext' => 'Add this text to the beginning of the page. Overrides text.',
'appendtext' => 'Add this text to the end of the page. Overrides text',
'undo' => 'Undo this revision. Overrides text, prependtext and appendtext',
'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision',
);
}
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array (
@@ -363,6 +470,6 @@ class ApiEditPage extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiEditPage.php 50220 2009-05-05 14:07:59Z tstarling $';
+ return __CLASS__ . ': $Id: ApiEditPage.php 62600 2010-02-16 22:01:38Z reedy $';
}
}
diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php
index 9bb504fb..912480ef 100644
--- a/includes/api/ApiEmailUser.php
+++ b/includes/api/ApiEmailUser.php
@@ -22,19 +22,18 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
-
/**
* @ingroup API
*/
class ApiEmailUser extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
@@ -49,8 +48,6 @@ class ApiEmailUser extends ApiBase {
$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'] );
@@ -61,8 +58,7 @@ class ApiEmailUser extends ApiBase {
$error = EmailUserForm::getPermissionsError( $wgUser, $params['token'] );
if ( $error )
$this->dieUsageMsg( array( $error ) );
-
-
+
$form = new EmailUserForm( $targetUser, $params['text'], $params['subject'], $params['ccme'] );
$retval = $form->doSubmit();
if ( is_null( $retval ) )
@@ -74,7 +70,9 @@ class ApiEmailUser extends ApiBase {
$this->getResult()->addValue( null, $this->getModuleName(), $result );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
@@ -105,6 +103,18 @@ class ApiEmailUser extends ApiBase {
'Email a user.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'usermaildisabled' ),
+ array( 'missingparam', 'target' ),
+ array( 'missingparam', 'text' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array (
@@ -113,7 +123,7 @@ class ApiEmailUser extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiEmailUser.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiEmailUser.php 62599 2010-02-16 21:59:16Z reedy $';
}
-}
+}
\ No newline at end of file
diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php
index afb11402..d0c00db7 100644
--- a/includes/api/ApiExpandTemplates.php
+++ b/includes/api/ApiExpandTemplates.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -37,8 +37,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiExpandTemplates extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
@@ -48,9 +48,9 @@ class ApiExpandTemplates extends ApiBase {
// Get parameters
$params = $this->extractRequestParams();
- //Create title for parser
+ // Create title for parser
$title_obj = Title :: newFromText( $params['title'] );
- if(!$title_obj)
+ if ( !$title_obj )
$title_obj = Title :: newFromText( "API" ); // default
$result = $this->getResult();
@@ -58,6 +58,7 @@ class ApiExpandTemplates extends ApiBase {
// Parse text
global $wgParser;
$options = new ParserOptions();
+
if ( $params['generatexml'] )
{
$wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS );
@@ -69,7 +70,7 @@ class ApiExpandTemplates extends ApiBase {
}
$xml_result = array();
$result->setContent( $xml_result, $xml );
- $result->addValue( null, 'parsetree', $xml_result);
+ $result->addValue( null, 'parsetree', $xml_result );
}
$retval = $wgParser->preprocess( $params['text'], $title_obj, $options );
@@ -108,6 +109,6 @@ class ApiExpandTemplates extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiExpandTemplates.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiExpandTemplates.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php
index 0859232e..03d12800 100644
--- a/includes/api/ApiFeedWatchlist.php
+++ b/includes/api/ApiFeedWatchlist.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -37,15 +37,15 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiFeedWatchlist extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
/**
* This module uses a custom feed wrapper printer.
*/
public function getCustomPrinter() {
- return new ApiFormatFeedWrapper($this->getMain());
+ return new ApiFormatFeedWrapper( $this->getMain() );
}
/**
@@ -60,7 +60,7 @@ class ApiFeedWatchlist extends ApiBase {
$params = $this->extractRequestParams();
// limit to the number of hours going from now back
- $endTime = wfTimestamp(TS_MW, time() - intval($params['hours'] * 60 * 60));
+ $endTime = wfTimestamp( TS_MW, time() - intval( $params['hours'] * 60 * 60 ) );
$dbr = wfGetDB( DB_SLAVE );
// Prepare parameters for nested request
@@ -71,48 +71,56 @@ class ApiFeedWatchlist extends ApiBase {
'list' => 'watchlist',
'wlprop' => 'title|user|comment|timestamp',
'wldir' => 'older', // reverse order - from newest to oldest
- 'wlend' => $dbr->timestamp($endTime), // stop at this time
- 'wllimit' => (50 > $wgFeedLimit) ? $wgFeedLimit : 50
+ 'wlend' => $dbr->timestamp( $endTime ), // stop at this time
+ 'wllimit' => ( 50 > $wgFeedLimit ) ? $wgFeedLimit : 50
);
+ if ( !is_null( $params['wlowner'] ) ) {
+ $fauxReqArr['wlowner'] = $params['wlowner'];
+ }
+ if ( !is_null( $params['wltoken'] ) ) {
+ $fauxReqArr['wltoken'] = $params['wltoken'];
+ }
+
// Check for 'allrev' parameter, and if found, show all revisions to each page on wl.
- if ( ! is_null ( $params['allrev'] ) ) $fauxReqArr['wlallrev'] = '';
+ if ( !is_null ( $params['allrev'] ) ) {
+ $fauxReqArr['wlallrev'] = '';
+ }
// Create the request
$fauxReq = new FauxRequest ( $fauxReqArr );
// Execute
- $module = new ApiMain($fauxReq);
+ $module = new ApiMain( $fauxReq );
$module->execute();
// Get data array
$data = $module->getResultData();
- $feedItems = array ();
- foreach ((array)$data['query']['watchlist'] as $info) {
- $feedItems[] = $this->createFeedItem($info);
+ $feedItems = array();
+ foreach ( (array)$data['query']['watchlist'] as $info ) {
+ $feedItems[] = $this->createFeedItem( $info );
}
- $feedTitle = $wgSitename . ' - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
+ $feedTitle = $wgSitename . ' - ' . wfMsgForContent( 'watchlist' ) . ' [' . $wgContLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
- $feed = new $wgFeedClasses[$params['feedformat']] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
+ $feed = new $wgFeedClasses[$params['feedformat']] ( $feedTitle, htmlspecialchars( wfMsgForContent( 'watchlist' ) ), $feedUrl );
- ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
+ ApiFormatFeedWrapper :: setResult( $this->getResult(), $feed, $feedItems );
- } catch (Exception $e) {
+ } catch ( Exception $e ) {
// Error results should not be cached
- $this->getMain()->setCacheMaxAge(0);
+ $this->getMain()->setCacheMaxAge( 0 );
- $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent('watchlist') . ' [' . $wgContLanguageCode . ']';
+ $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent( 'watchlist' ) . ' [' . $wgContLanguageCode . ']';
$feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullUrl();
- $feedFormat = isset($params['feedformat']) ? $params['feedformat'] : 'rss';
- $feed = new $wgFeedClasses[$feedFormat] ($feedTitle, htmlspecialchars(wfMsgForContent('watchlist')), $feedUrl);
-
+ $feedFormat = isset( $params['feedformat'] ) ? $params['feedformat'] : 'rss';
+ $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, htmlspecialchars( wfMsgForContent( 'watchlist' ) ), $feedUrl );
- if ($e instanceof UsageException) {
+ if ( $e instanceof UsageException ) {
$errorCode = $e->getCodeString();
} else {
// Something is seriously wrong
@@ -120,14 +128,14 @@ class ApiFeedWatchlist extends ApiBase {
}
$errorText = $e->getMessage();
- $feedItems[] = new FeedItem("Error ($errorCode)", $errorText, "", "", "");
- ApiFormatFeedWrapper :: setResult($this->getResult(), $feed, $feedItems);
+ $feedItems[] = new FeedItem( "Error ($errorCode)", $errorText, "", "", "" );
+ ApiFormatFeedWrapper :: setResult( $this->getResult(), $feed, $feedItems );
}
}
- private function createFeedItem($info) {
+ private function createFeedItem( $info ) {
$titleStr = $info['title'];
- $title = Title :: newFromText($titleStr);
+ $title = Title :: newFromText( $titleStr );
$titleUrl = $title->getFullUrl();
$comment = isset( $info['comment'] ) ? $info['comment'] : null;
$timestamp = $info['timestamp'];
@@ -135,12 +143,12 @@ class ApiFeedWatchlist extends ApiBase {
$completeText = "$comment ($user)";
- return new FeedItem($titleStr, $completeText, $titleUrl, $timestamp, $user);
+ return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user );
}
public function getAllowedParams() {
global $wgFeedClasses;
- $feedFormatNames = array_keys($wgFeedClasses);
+ $feedFormatNames = array_keys( $wgFeedClasses );
return array (
'feedformat' => array (
ApiBase :: PARAM_DFLT => 'rss',
@@ -152,7 +160,13 @@ class ApiFeedWatchlist extends ApiBase {
ApiBase :: PARAM_MIN => 1,
ApiBase :: PARAM_MAX => 72,
),
- 'allrev' => null
+ 'allrev' => null,
+ 'wlowner' => array (
+ ApiBase :: PARAM_TYPE => 'user'
+ ),
+ 'wltoken' => array (
+ ApiBase :: PARAM_TYPE => 'string'
+ )
);
}
@@ -160,7 +174,9 @@ class ApiFeedWatchlist extends ApiBase {
return array (
'feedformat' => 'The format of the feed',
'hours' => 'List pages modified within this many hours from now',
- 'allrev' => 'Include multiple revisions of the same page within given timeframe.'
+ 'allrev' => 'Include multiple revisions of the same page within given timeframe.',
+ 'wlowner' => "The user whose watchlist you want (must be accompanied by wltoken if it's not you)",
+ 'wltoken' => 'Security token that requested user set in their preferences'
);
}
@@ -175,6 +191,6 @@ class ApiFeedWatchlist extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFeedWatchlist.php 46848 2009-02-05 15:31:06Z catrope $';
+ return __CLASS__ . ': $Id: ApiFeedWatchlist.php 69357 2010-07-14 22:39:23Z mah $';
}
}
diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php
index cc7434c6..de211fe9 100644
--- a/includes/api/ApiFormatBase.php
+++ b/includes/api/ApiFormatBase.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -36,6 +36,7 @@ if (!defined('MEDIAWIKI')) {
abstract class ApiFormatBase extends ApiBase {
private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp, $mCleared;
+ private $mBufferResult = false, $mBuffer;
/**
* Constructor
@@ -43,15 +44,15 @@ abstract class ApiFormatBase extends ApiBase {
* @param $main ApiMain
* @param $format string Format name
*/
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
- $this->mIsHtml = (substr($format, -2, 2) === 'fm'); // ends with 'fm'
- if ($this->mIsHtml)
- $this->mFormat = substr($format, 0, -2); // remove ending 'fm'
+ $this->mIsHtml = ( substr( $format, - 2, 2 ) === 'fm' ); // ends with 'fm'
+ if ( $this->mIsHtml )
+ $this->mFormat = substr( $format, 0, - 2 ); // remove ending 'fm'
else
$this->mFormat = $format;
- $this->mFormat = strtoupper($this->mFormat);
+ $this->mFormat = strtoupper( $this->mFormat );
$this->mCleared = false;
}
@@ -102,29 +103,39 @@ abstract class ApiFormatBase extends ApiBase {
}
/**
+ * Whether this formatter can format the help message in a nice way.
+ * By default, this returns the same as getIsHtml().
+ * When action=help is set explicitly, the help will always be shown
+ * @return bool
+ */
+ public function getWantsHelp() {
+ return $this->getIsHtml();
+ }
+
+ /**
* Initialize the printer function and prepare the output headers, etc.
* This method must be the first outputing method during execution.
* A help screen's header is printed for the HTML-based output
* @param $isError bool Whether an error message is printed
*/
- function initPrinter($isError) {
+ function initPrinter( $isError ) {
$isHtml = $this->getIsHtml();
$mime = $isHtml ? 'text/html' : $this->getMimeType();
$script = wfScript( 'api' );
// Some printers (ex. Feed) do their own header settings,
// in which case $mime will be set to null
- if (is_null($mime))
+ if ( is_null( $mime ) )
return; // skip any initialization
- header("Content-Type: $mime; charset=utf-8");
+ header( "Content-Type: $mime; charset=utf-8" );
- if ($isHtml) {
+ if ( $isHtml ) {
?>
<!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>
@@ -134,12 +145,12 @@ abstract class ApiFormatBase extends ApiBase {
<?php
- if( !$isError ) {
+ if ( !$isError ) {
?>
-<br/>
+<br />
<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/>
+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
<a href='<?php echo( $script ); ?>'>API help</a> for more information.
</small>
@@ -159,7 +170,7 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
* Finish printing. Closes HTML tags.
*/
public function closePrinter() {
- if ($this->getIsHtml()) {
+ if ( $this->getIsHtml() ) {
?>
</pre>
@@ -177,15 +188,16 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
* when format name ends in 'fm'.
* @param $text string
*/
- public function printText($text) {
- if ($this->getIsHtml())
- echo $this->formatHTML($text);
- else
- {
+ public function printText( $text ) {
+ if ( $this->mBufferResult ) {
+ $this->mBuffer = $text;
+ } elseif ( $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)
+ if ( !$this->mCleared )
{
ob_clean();
$this->mCleared = true;
@@ -195,6 +207,19 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
}
/**
+ * Get the contents of the buffer.
+ */
+ public function getBuffer() {
+ return $this->mBuffer;
+ }
+ /**
+ * Set the flag to buffer the result instead of printing it.
+ */
+ public function setBufferResult( $value ) {
+ $this->mBufferResult = $value;
+ }
+
+ /**
* Sets whether the pretty-printer should format *bold* and $italics$
* @param $help bool
*/
@@ -208,25 +233,25 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
* @param $text string
* @return string
*/
- protected function formatHTML($text) {
+ protected function formatHTML( $text ) {
global $wgUrlProtocols;
-
+
// Escape everything first for full coverage
- $text = htmlspecialchars($text);
+ $text = htmlspecialchars( $text );
// encode all comments or tags as safe blue strings
- $text = preg_replace('/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text);
+ $text = preg_replace( '/\&lt;(!--.*?--|.*?)\&gt;/', '<span style="color:blue;">&lt;\1&gt;</span>', $text );
// identify URLs
- $protos = implode("|", $wgUrlProtocols);
- # This regex hacks around bug 13218 (&quot; included in the URL)
- $text = preg_replace("#(($protos).*?)(&quot;)?([ \\'\"<>\n]|&lt;|&gt;|&quot;)#", '<a href="\\1">\\1</a>\\3\\4', $text);
+ $protos = implode( "|", $wgUrlProtocols );
+ // This regex hacks around bug 13218 (&quot; included in the URL)
+ $text = preg_replace( "#(($protos).*?)(&quot;)?([ \\'\"<>\n]|&lt;|&gt;|&quot;)#", '<a href="\\1">\\1</a>\\3\\4', $text );
// identify requests to api.php
- $text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '<a href="\\0">\\0</a>', $text);
- if( $this->mHelp ) {
+ $text = preg_replace( "#api\\.php\\?[^ \\()<\n\t]+#", '<a href="\\0">\\0</a>', $text );
+ if ( $this->mHelp ) {
// make strings inside * bold
- $text = preg_replace("#\\*[^<>\n]+\\*#", '<b>\\0</b>', $text);
+ $text = preg_replace( "#\\*[^<>\n]+\\*#", '<b>\\0</b>', $text );
// make strings inside $ italic
- $text = preg_replace("#\\$[^<>\n]+\\$#", '<b><i>\\0</i></b>', $text);
+ $text = preg_replace( "#\\$[^<>\n]+\\$#", '<b><i>\\0</i></b>', $text );
}
/* Temporary fix for bad links in help messages. As a special case,
@@ -248,7 +273,7 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
}
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 48521 2009-03-18 19:25:29Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 62367 2010-02-12 14:09:42Z siebrand $';
}
}
@@ -258,8 +283,8 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or
*/
class ApiFormatFeedWrapper extends ApiFormatBase {
- public function __construct($main) {
- parent :: __construct($main, 'feed');
+ public function __construct( $main ) {
+ parent :: __construct( $main, 'feed' );
}
/**
@@ -268,15 +293,15 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
* @param $feed object an instance of one of the $wgFeedClasses classes
* @param $feedItems array of FeedItem objects
*/
- public static function setResult($result, $feed, $feedItems) {
+ public static function setResult( $result, $feed, $feedItems ) {
// Store output in the Result data.
// This way we can check during execution if any error has occured
// Disable size checking for this because we can't continue
// cleanly; size checking would cause more problems than it'd
// solve
$result->disableSizeCheck();
- $result->addValue(null, '_feed', $feed);
- $result->addValue(null, '_feeditems', $feedItems);
+ $result->addValue( null, '_feed', $feed );
+ $result->addValue( null, '_feeditems', $feedItems );
$result->enableSizeCheck();
}
@@ -301,13 +326,13 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
*/
public function execute() {
$data = $this->getResultData();
- if (isset ($data['_feed']) && isset ($data['_feeditems'])) {
+ if ( isset ( $data['_feed'] ) && isset ( $data['_feeditems'] ) ) {
$feed = $data['_feed'];
$items = $data['_feeditems'];
$feed->outHeader();
- foreach ($items as & $item)
- $feed->outItem($item);
+ foreach ( $items as & $item )
+ $feed->outItem( $item );
$feed->outFooter();
} else {
// Error has occured, print something useful
@@ -316,6 +341,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatBase.php 48521 2009-03-18 19:25:29Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatBase.php 62367 2010-02-12 14:09:42Z siebrand $';
}
} \ No newline at end of file
diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php
index 254c140b..26afd329 100644
--- a/includes/api/ApiFormatDbg.php
+++ b/includes/api/ApiFormatDbg.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -33,19 +33,19 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiFormatDbg extends ApiFormatBase {
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
}
public function getMimeType() {
- # This looks like it should be text/plain, but IE7 is so
- # brain-damaged it tries to parse text/plain as HTML if it
- # contains HTML tags. Using MIME text/text works around this bug
+ // This looks like it should be text/plain, but IE7 is so
+ // brain-damaged it tries to parse text/plain as HTML if it
+ // contains HTML tags. Using MIME text/text works around this bug
return 'text/text';
}
public function execute() {
- $this->printText(var_export($this->getResultData(), true));
+ $this->printText( var_export( $this->getResultData(), true ) );
}
public function getDescription() {
@@ -53,6 +53,6 @@ class ApiFormatDbg extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatDbg.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatDbg.php 61444 2010-01-23 22:52:40Z reedy $';
}
}
diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php
index 7b5a02a4..69686bfb 100644
--- a/includes/api/ApiFormatJson.php
+++ b/includes/api/ApiFormatJson.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -35,12 +35,17 @@ class ApiFormatJson extends ApiFormatBase {
private $mIsRaw;
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
- $this->mIsRaw = ($format === 'rawfm');
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
+ $this->mIsRaw = ( $format === 'rawfm' );
}
public function getMimeType() {
+ $params = $this->extractRequestParams();
+ // callback:
+ if ( $params['callback'] ) {
+ return 'text/javascript';
+ }
return 'application/json';
}
@@ -48,30 +53,29 @@ class ApiFormatJson extends ApiFormatBase {
return $this->mIsRaw;
}
+ public function getWantsHelp() {
+ // Help is always ugly in JSON
+ return false;
+ }
+
public function execute() {
$prefix = $suffix = "";
$params = $this->extractRequestParams();
$callback = $params['callback'];
- if(!is_null($callback)) {
- $prefix = preg_replace("/[^][.\\'\\\"_A-Za-z0-9]/", "", $callback ) . "(";
+ if ( !is_null( $callback ) ) {
+ $prefix = preg_replace( "/[^][.\\'\\\"_A-Za-z0-9]/", "", $callback ) . "(";
$suffix = ")";
}
-
- // Some versions of PHP have a broken json_encode, see PHP bug
- // 46944. Test encoding an affected character (U+20000) to
- // avoid this.
- if (!function_exists('json_encode') || $this->getIsHtml() || strtolower(json_encode("\xf0\xa0\x80\x80")) != '"\ud840\udc00"') {
- $json = new Services_JSON();
- $this->printText($prefix . $json->encode($this->getResultData(), $this->getIsHtml()) . $suffix);
- } else {
- $this->printText($prefix . json_encode($this->getResultData()) . $suffix);
- }
+ $this->printText(
+ $prefix .
+ FormatJson::encode( $this->getResultData(), $this->getIsHtml() ) .
+ $suffix );
}
public function getAllowedParams() {
return array (
- 'callback' => null
+ 'callback' => null,
);
}
@@ -82,13 +86,13 @@ class ApiFormatJson extends ApiFormatBase {
}
public function getDescription() {
- if ($this->mIsRaw)
+ if ( $this->mIsRaw )
return 'Output data with the debuging elements in JSON format' . parent :: getDescription();
else
return 'Output data in JSON format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatJson.php 48713 2009-03-23 19:58:07Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatJson.php 62354 2010-02-12 06:44:16Z mah $';
}
}
diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php
index 163d3028..dd03c300 100644
--- a/includes/api/ApiFormatPhp.php
+++ b/includes/api/ApiFormatPhp.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -33,8 +33,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiFormatPhp extends ApiFormatBase {
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
}
public function getMimeType() {
@@ -42,7 +42,7 @@ class ApiFormatPhp extends ApiFormatBase {
}
public function execute() {
- $this->printText(serialize($this->getResultData()));
+ $this->printText( serialize( $this->getResultData() ) );
}
public function getDescription() {
@@ -50,6 +50,6 @@ class ApiFormatPhp extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatPhp.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatPhp.php 60930 2010-01-11 15:55:52Z simetrical $';
}
}
diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php
index 51025448..8bb66aea 100644
--- a/includes/api/ApiFormatRaw.php
+++ b/includes/api/ApiFormatRaw.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -39,33 +39,37 @@ class ApiFormatRaw extends ApiFormatBase {
* @param $main ApiMain object
* @param $errorFallback Formatter object to fall back on for errors
*/
- public function __construct($main, $errorFallback) {
- parent :: __construct($main, 'raw');
+ public function __construct( $main, $errorFallback ) {
+ parent :: __construct( $main, 'raw' );
$this->mErrorFallback = $errorFallback;
}
public function getMimeType() {
$data = $this->getResultData();
- if(isset($data['error']))
+
+ if ( isset( $data['error'] ) )
return $this->mErrorFallback->getMimeType();
- if(!isset($data['mime']))
- ApiBase::dieDebug(__METHOD__, "No MIME type set for raw formatter");
+
+ if ( !isset( $data['mime'] ) )
+ ApiBase::dieDebug( __METHOD__, "No MIME type set for raw formatter" );
+
return $data['mime'];
}
public function execute() {
$data = $this->getResultData();
- if(isset($data['error']))
+ if ( isset( $data['error'] ) )
{
$this->mErrorFallback->execute();
return;
}
- if(!isset($data['text']))
- ApiBase::dieDebug(__METHOD__, "No text given for raw formatter");
- $this->printText($data['text']);
+
+ if ( !isset( $data['text'] ) )
+ ApiBase::dieDebug( __METHOD__, "No text given for raw formatter" );
+ $this->printText( $data['text'] );
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatRaw.php 48629 2009-03-20 11:40:54Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatRaw.php 61437 2010-01-23 22:26:40Z reedy $';
}
}
diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php
index 5f608d5c..1627dde6 100644
--- a/includes/api/ApiFormatTxt.php
+++ b/includes/api/ApiFormatTxt.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -33,19 +33,19 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiFormatTxt extends ApiFormatBase {
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
}
public function getMimeType() {
- # This looks like it should be text/plain, but IE7 is so
- # brain-damaged it tries to parse text/plain as HTML if it
- # contains HTML tags. Using MIME text/text works around this bug
+ // This looks like it should be text/plain, but IE7 is so
+ // brain-damaged it tries to parse text/plain as HTML if it
+ // contains HTML tags. Using MIME text/text works around this bug
return 'text/text';
}
public function execute() {
- $this->printText(print_r($this->getResultData(), true));
+ $this->printText( print_r( $this->getResultData(), true ) );
}
public function getDescription() {
@@ -53,6 +53,6 @@ class ApiFormatTxt extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatTxt.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatTxt.php 61444 2010-01-23 22:52:40Z reedy $';
}
}
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index a716373d..e95e540b 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -33,8 +33,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiFormatWddx extends ApiFormatBase {
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
}
public function getMimeType() {
@@ -46,67 +46,66 @@ class ApiFormatWddx extends ApiFormatBase {
// PHP bug 45314. Test encoding an affected character (U+00A0)
// to avoid this.
$expected = "<wddxPacket version='1.0'><header/><data><string>\xc2\xa0</string></data></wddxPacket>";
- if (function_exists('wddx_serialize_value')
+ if ( function_exists( 'wddx_serialize_value' )
&& !$this->getIsHtml()
- && wddx_serialize_value("\xc2\xa0") == $expected) {
- $this->printText(wddx_serialize_value($this->getResultData()));
+ && wddx_serialize_value( "\xc2\xa0" ) == $expected ) {
+ $this->printText( wddx_serialize_value( $this->getResultData() ) );
} else {
// Don't do newlines and indentation if we weren't asked
// for pretty output
- $nl = ($this->getIsHtml() ? "" : "\n");
+ $nl = ( $this->getIsHtml() ? "" : "\n" );
$indstr = " ";
- $this->printText("<?xml version=\"1.0\"?>$nl");
- $this->printText("<wddxPacket version=\"1.0\">$nl");
- $this->printText("$indstr<header/>$nl");
- $this->printText("$indstr<data>$nl");
- $this->slowWddxPrinter($this->getResultData(), 4);
- $this->printText("$indstr</data>$nl");
- $this->printText("</wddxPacket>$nl");
+ $this->printText( "<?xml version=\"1.0\"?>$nl" );
+ $this->printText( "<wddxPacket version=\"1.0\">$nl" );
+ $this->printText( "$indstr<header/>$nl" );
+ $this->printText( "$indstr<data>$nl" );
+ $this->slowWddxPrinter( $this->getResultData(), 4 );
+ $this->printText( "$indstr</data>$nl" );
+ $this->printText( "</wddxPacket>$nl" );
}
}
/**
* Recursively go through the object and output its data in WDDX format.
*/
- function slowWddxPrinter($elemValue, $indent = 0) {
- $indstr = ($this->getIsHtml() ? "" : str_repeat(' ', $indent));
- $indstr2 = ($this->getIsHtml() ? "" : str_repeat(' ', $indent + 2));
- $nl = ($this->getIsHtml() ? "" : "\n");
- switch (gettype($elemValue)) {
+ function slowWddxPrinter( $elemValue, $indent = 0 ) {
+ $indstr = ( $this->getIsHtml() ? "" : str_repeat( ' ', $indent ) );
+ $indstr2 = ( $this->getIsHtml() ? "" : str_repeat( ' ', $indent + 2 ) );
+ $nl = ( $this->getIsHtml() ? "" : "\n" );
+ switch ( gettype( $elemValue ) ) {
case 'array' :
// Check whether we've got an associative array (<struct>)
// or a regular array (<array>)
- $cnt = count($elemValue);
- if($cnt == 0 || array_keys($elemValue) === range(0, $cnt - 1)) {
+ $cnt = count( $elemValue );
+ if ( $cnt == 0 || array_keys( $elemValue ) === range( 0, $cnt - 1 ) ) {
// Regular array
- $this->printText($indstr . Xml::element('array', array(
- 'length' => $cnt
- ), null) . $nl);
- foreach($elemValue as $subElemValue)
- $this->slowWddxPrinter($subElemValue, $indent + 2);
- $this->printText("$indstr</array>$nl");
+ $this->printText( $indstr . Xml::element( 'array', array(
+ 'length' => $cnt ), null ) . $nl );
+ foreach ( $elemValue as $subElemValue )
+ $this->slowWddxPrinter( $subElemValue, $indent + 2 );
+ $this->printText( "$indstr</array>$nl" );
} else {
// Associative array (<struct>)
- $this->printText("$indstr<struct>$nl");
- foreach($elemValue as $subElemName => $subElemValue) {
- $this->printText($indstr2 . Xml::element('var', array(
+ $this->printText( "$indstr<struct>$nl" );
+ foreach ( $elemValue as $subElemName => $subElemValue ) {
+ $this->printText( $indstr2 . Xml::element( 'var', array(
'name' => $subElemName
- ), null) . $nl);
- $this->slowWddxPrinter($subElemValue, $indent + 4);
- $this->printText("$indstr2</var>$nl");
+ ), null ) . $nl );
+ $this->slowWddxPrinter( $subElemValue, $indent + 4 );
+ $this->printText( "$indstr2</var>$nl" );
}
- $this->printText("$indstr</struct>$nl");
+ $this->printText( "$indstr</struct>$nl" );
}
break;
case 'integer' :
case 'double' :
- $this->printText($indstr . Xml::element('number', null, $elemValue) . $nl);
+ $this->printText( $indstr . Xml::element( 'number', null, $elemValue ) . $nl );
break;
case 'string' :
- $this->printText($indstr . Xml::element('string', null, $elemValue) . $nl);
+ $this->printText( $indstr . Xml::element( 'string', null, $elemValue ) . $nl );
break;
default :
- ApiBase :: dieDebug(__METHOD__, 'Unknown type ' . gettype($elemValue));
+ ApiBase :: dieDebug( __METHOD__, 'Unknown type ' . gettype( $elemValue ) );
}
}
@@ -115,6 +114,6 @@ class ApiFormatWddx extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatWddx.php 48716 2009-03-23 20:06:16Z catrope $';
+ return __CLASS__ . ': $Id: ApiFormatWddx.php 61437 2010-01-23 22:26:40Z reedy $';
}
}
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index 35b412c9..a3758a49 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -35,9 +35,10 @@ class ApiFormatXml extends ApiFormatBase {
private $mRootElemName = 'api';
private $mDoubleQuote = false;
+ private $mXslt = null;
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
}
public function getMimeType() {
@@ -48,16 +49,22 @@ class ApiFormatXml extends ApiFormatBase {
return true;
}
- public function setRootElement($rootElemName) {
+ public function setRootElement( $rootElemName ) {
$this->mRootElemName = $rootElemName;
}
public function execute() {
$params = $this->extractRequestParams();
$this->mDoubleQuote = $params['xmldoublequote'];
-
- $this->printText('<?xml version="1.0"?>');
- $this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null);
+ $this->mXslt = $params['xslt'];
+
+ $this->printText( '<?xml version="1.0"?>' );
+ if ( !is_null( $this->mXslt ) )
+ $this->addXslt();
+ $this->printText( self::recXmlPrint( $this->mRootElemName,
+ $this->getResultData(),
+ $this->getIsHtml() ? - 2 : null,
+ $this->mDoubleQuote ) );
}
/**
@@ -73,22 +80,23 @@ class ApiFormatXml extends ApiFormatBase {
* If neither key is found, all keys become element names, and values become element content.
* The method is recursive, so the same rules apply to any sub-arrays.
*/
- function recXmlPrint($elemName, $elemValue, $indent) {
- if (!is_null($indent)) {
+ public static function recXmlPrint( $elemName, $elemValue, $indent, $doublequote = false ) {
+ $retval = '';
+ if ( !is_null( $indent ) ) {
$indent += 2;
- $indstr = "\n" . str_repeat(" ", $indent);
+ $indstr = "\n" . str_repeat( " ", $indent );
} else {
$indstr = '';
}
- $elemName = str_replace(' ', '_', $elemName);
+ $elemName = str_replace( ' ', '_', $elemName );
- switch (gettype($elemValue)) {
+ switch ( gettype( $elemValue ) ) {
case 'array' :
- if (isset ($elemValue['*'])) {
+ if ( isset ( $elemValue['*'] ) ) {
$subElemContent = $elemValue['*'];
- if ($this->mDoubleQuote)
- $subElemContent = $this->doubleQuote($subElemContent);
- unset ($elemValue['*']);
+ if ( $doublequote )
+ $subElemContent = Sanitizer::encodeAttribute( $subElemContent );
+ unset ( $elemValue['*'] );
// Add xml:space="preserve" to the
// element so XML parsers will leave
@@ -98,80 +106,95 @@ class ApiFormatXml extends ApiFormatBase {
$subElemContent = null;
}
- if (isset ($elemValue['_element'])) {
+ if ( isset ( $elemValue['_element'] ) ) {
$subElemIndName = $elemValue['_element'];
- unset ($elemValue['_element']);
+ unset ( $elemValue['_element'] );
} else {
$subElemIndName = null;
}
$indElements = array ();
$subElements = array ();
- foreach ($elemValue as $subElemId => & $subElemValue) {
- if (is_string($subElemValue) && $this->mDoubleQuote)
- $subElemValue = $this->doubleQuote($subElemValue);
+ foreach ( $elemValue as $subElemId => & $subElemValue ) {
+ if ( is_string( $subElemValue ) && $doublequote )
+ $subElemValue = Sanitizer::encodeAttribute( $subElemValue );
- if (gettype($subElemId) === 'integer') {
+ if ( gettype( $subElemId ) === 'integer' ) {
$indElements[] = $subElemValue;
- unset ($elemValue[$subElemId]);
- } elseif (is_array($subElemValue)) {
+ unset ( $elemValue[$subElemId] );
+ } elseif ( is_array( $subElemValue ) ) {
$subElements[$subElemId] = $subElemValue;
- unset ($elemValue[$subElemId]);
+ unset ( $elemValue[$subElemId] );
}
}
- if (is_null($subElemIndName) && count($indElements))
- ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName().");
+ if ( is_null( $subElemIndName ) && count( $indElements ) )
+ ApiBase :: dieDebug( __METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()." );
- if (count($subElements) && count($indElements) && !is_null($subElemContent))
- ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has content and subelements");
+ if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) )
+ ApiBase :: dieDebug( __METHOD__, "($elemName, ...) has content and subelements" );
- if (!is_null($subElemContent)) {
- $this->printText($indstr . Xml::element($elemName, $elemValue, $subElemContent));
- } elseif (!count($indElements) && !count($subElements)) {
- $this->printText($indstr . Xml::element($elemName, $elemValue));
+ if ( !is_null( $subElemContent ) ) {
+ $retval .= $indstr . Xml::element( $elemName, $elemValue, $subElemContent );
+ } elseif ( !count( $indElements ) && !count( $subElements ) ) {
+ $retval .= $indstr . Xml::element( $elemName, $elemValue );
} else {
- $this->printText($indstr . Xml::element($elemName, $elemValue, null));
+ $retval .= $indstr . Xml::element( $elemName, $elemValue, null );
- foreach ($subElements as $subElemId => & $subElemValue)
- $this->recXmlPrint($subElemId, $subElemValue, $indent);
+ foreach ( $subElements as $subElemId => & $subElemValue )
+ $retval .= self::recXmlPrint( $subElemId, $subElemValue, $indent );
- foreach ($indElements as $subElemId => & $subElemValue)
- $this->recXmlPrint($subElemIndName, $subElemValue, $indent);
+ foreach ( $indElements as $subElemId => & $subElemValue )
+ $retval .= self::recXmlPrint( $subElemIndName, $subElemValue, $indent );
- $this->printText($indstr . Xml::closeElement($elemName));
+ $retval .= $indstr . Xml::closeElement( $elemName );
}
break;
case 'object' :
// ignore
break;
default :
- $this->printText($indstr . Xml::element($elemName, null, $elemValue));
+ $retval .= $indstr . Xml::element( $elemName, null, $elemValue );
break;
}
+ return $retval;
}
- private function doubleQuote( $text ) {
- return Sanitizer::encodeAttribute( $text );
+ function addXslt() {
+ $nt = Title::newFromText( $this->mXslt );
+ if ( is_null( $nt ) || !$nt->exists() ) {
+ $this->setWarning( 'Invalid or non-existent stylesheet specified' );
+ return;
+ }
+ if ( $nt->getNamespace() != NS_MEDIAWIKI ) {
+ $this->setWarning( 'Stylesheet should be in the MediaWiki namespace.' );
+ return;
+ }
+ if ( substr( $nt->getText(), - 4 ) !== '.xsl' ) {
+ $this->setWarning( 'Stylesheet should have .xsl extension.' );
+ return;
+ }
+ $this->printText( '<?xml-stylesheet href="' . $nt->escapeLocalURL( 'action=raw' ) . '" type="text/xsl" ?>' );
}
-
+
public function getAllowedParams() {
return array (
- 'xmldoublequote' => false
+ 'xmldoublequote' => false,
+ 'xslt' => null,
);
}
public function getParamDescription() {
return array (
'xmldoublequote' => 'If specified, double quotes all attributes and content.',
+ 'xslt' => 'If specified, adds <xslt> as stylesheet',
);
}
-
public function getDescription() {
return 'Output data in XML format' . parent :: getDescription();
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatXml.php 50217 2009-05-05 13:12:16Z tstarling $';
+ return __CLASS__ . ': $Id: ApiFormatXml.php 62402 2010-02-13 00:09:05Z reedy $';
}
}
diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php
index cc255c63..39381b0f 100644
--- a/includes/api/ApiFormatYaml.php
+++ b/includes/api/ApiFormatYaml.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiFormatBase.php');
+ require_once ( 'ApiFormatBase.php' );
}
/**
@@ -33,8 +33,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiFormatYaml extends ApiFormatBase {
- public function __construct($main, $format) {
- parent :: __construct($main, $format);
+ public function __construct( $main, $format ) {
+ parent :: __construct( $main, $format );
}
public function getMimeType() {
@@ -42,7 +42,7 @@ class ApiFormatYaml extends ApiFormatBase {
}
public function execute() {
- $this->printText(Spyc :: YAMLDump($this->getResultData()));
+ $this->printText( Spyc :: YAMLDump( $this->getResultData() ) );
}
public function getDescription() {
@@ -50,6 +50,6 @@ class ApiFormatYaml extends ApiFormatBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiFormatYaml.php 35098 2008-05-20 17:13:28Z ialex $';
+ return __CLASS__ . ': $Id: ApiFormatYaml.php 60930 2010-01-11 15:55:52Z simetrical $';
}
}
diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php
index f16b2c8a..30f860dd 100644
--- a/includes/api/ApiFormatYaml_spyc.php
+++ b/includes/api/ApiFormatYaml_spyc.php
@@ -38,9 +38,9 @@ class Spyc {
* @param $indent Integer: Pass in false to use the default, which is 2
* @param $wordwrap Integer: Pass in 0 for no wordwrap, false for default (40)
*/
- public static function YAMLDump($array,$indent = false,$wordwrap = false) {
+ public static function YAMLDump( $array, $indent = false, $wordwrap = false ) {
$spyc = new Spyc;
- return $spyc->dump($array,$indent,$wordwrap);
+ return $spyc->dump( $array, $indent, $wordwrap );
}
/**
@@ -63,18 +63,18 @@ class Spyc {
* @param $indent Integer: Pass in false to use the default, which is 2
* @param $wordwrap Integer: Pass in 0 for no wordwrap, false for default (40)
*/
- function dump($array,$indent = false,$wordwrap = false) {
+ function dump( $array, $indent = false, $wordwrap = false ) {
// Dumps to some very clean YAML. We'll have to add some more features
// and options soon. And better support for folding.
// New features and options.
- if ($indent === false or !is_numeric($indent)) {
+ if ( $indent === false or !is_numeric( $indent ) ) {
$this->_dumpIndent = 2;
} else {
$this->_dumpIndent = $indent;
}
- if ($wordwrap === false or !is_numeric($wordwrap)) {
+ if ( $wordwrap === false or !is_numeric( $wordwrap ) ) {
$this->_dumpWordWrap = 40;
} else {
$this->_dumpWordWrap = $wordwrap;
@@ -84,8 +84,8 @@ class Spyc {
$string = "---\n";
// Start at the base of the array and move through it.
- foreach ($array as $key => $value) {
- $string .= $this->_yamlize($key,$value,0);
+ foreach ( $array as $key => $value ) {
+ $string .= $this->_yamlize( $key, $value, 0 );
}
return $string;
}
@@ -110,18 +110,18 @@ class Spyc {
* @param $value The value of the item
* @param $indent The indent of the current node
*/
- private function _yamlize($key,$value,$indent) {
- if (is_array($value)) {
+ private function _yamlize( $key, $value, $indent ) {
+ if ( is_array( $value ) ) {
// It has children. What to do?
// Make it the right kind of item
- $string = $this->_dumpNode($key,NULL,$indent);
+ $string = $this->_dumpNode( $key, null, $indent );
// Add the indent
$indent += $this->_dumpIndent;
// Yamlize the array
- $string .= $this->_yamlizeArray($value,$indent);
- } elseif (!is_array($value)) {
+ $string .= $this->_yamlizeArray( $value, $indent );
+ } elseif ( !is_array( $value ) ) {
// It doesn't have children. Yip.
- $string = $this->_dumpNode($key,$value,$indent);
+ $string = $this->_dumpNode( $key, $value, $indent );
}
return $string;
}
@@ -132,11 +132,11 @@ class Spyc {
* @param $array The array you want to convert
* @param $indent The indent of the current level
*/
- private function _yamlizeArray($array,$indent) {
- if (is_array($array)) {
+ private function _yamlizeArray( $array, $indent ) {
+ if ( is_array( $array ) ) {
$string = '';
- foreach ($array as $key => $value) {
- $string .= $this->_yamlize($key,$value,$indent);
+ foreach ( $array as $key => $value ) {
+ $string .= $this->_yamlize( $key, $value, $indent );
}
return $string;
} else {
@@ -150,16 +150,15 @@ class Spyc {
* @param $value The string to check
* @return bool
*/
- function _needLiteral($value) {
- # Check whether the string contains # or : or begins with any of:
- # [ - ? , [ ] { } ! * & | > ' " % @ ` ]
- # or is a number or contains newlines
- return (bool)(gettype($value) == "string" &&
- (is_numeric($value) ||
- strpos($value, "\n") ||
- preg_match("/[#:]/", $value) ||
- preg_match("/^[-?,[\]{}!*&|>'\"%@`]/", $value)));
-
+ function _needLiteral( $value ) {
+ // Check whether the string contains # or : or begins with any of:
+ // [ - ? , [ ] { } ! * & | > ' " % @ ` ]
+ // or is a number or contains newlines
+ return (bool)( gettype( $value ) == "string" &&
+ ( is_numeric( $value ) ||
+ strpos( $value, "\n" ) ||
+ preg_match( "/[#:]/", $value ) ||
+ preg_match( "/^[-?,[\]{}!*&|>'\"%@`]/", $value ) ) );
}
/**
@@ -169,25 +168,28 @@ class Spyc {
* @param $value The value of the item
* @param $indent The indent of the current node
*/
- private function _dumpNode($key,$value,$indent) {
+ private function _dumpNode( $key, $value, $indent ) {
// do some folding here, for blocks
- if ($this->_needLiteral($value)) {
- $value = $this->_doLiteralBlock($value,$indent);
+ if ( $this->_needLiteral( $value ) ) {
+ $value = $this->_doLiteralBlock( $value, $indent );
} else {
- $value = $this->_doFolding($value,$indent);
+ $value = $this->_doFolding( $value, $indent );
}
- $spaces = str_repeat(' ',$indent);
+ $spaces = str_repeat( ' ', $indent );
- if (is_int($key)) {
+ if ( is_int( $key ) ) {
// It's a sequence
- if ($value !== '' && !is_null($value))
- $string = $spaces.'- '.$value."\n";
+ if ( $value !== '' && !is_null( $value ) )
+ $string = $spaces . '- ' . $value . "\n";
else
$string = $spaces . "-\n";
} else {
+ if ($key == '*') //bug 21922 - Quote asterix used as keys
+ $key = "'*'";
+
// It's mapped
- if ($value !== '' && !is_null($value))
+ if ( $value !== '' && !is_null( $value ) )
$string = $spaces . $key . ': ' . $value . "\n";
else
$string = $spaces . $key . ":\n";
@@ -201,13 +203,13 @@ class Spyc {
* @param $value
* @param $indent int The value of the indent
*/
- private function _doLiteralBlock($value,$indent) {
- $exploded = explode("\n",$value);
- $newValue = '|';
+ private function _doLiteralBlock( $value, $indent ) {
+ $exploded = explode( "\n", $value );
+ $newValue = '|-';
$indent += $this->_dumpIndent;
- $spaces = str_repeat(' ',$indent);
- foreach ($exploded as $line) {
- $newValue .= "\n" . $spaces . trim($line);
+ $spaces = str_repeat( ' ', $indent );
+ foreach ( $exploded as $line ) {
+ $newValue .= "\n" . $spaces . trim( $line );
}
return $newValue;
}
@@ -217,17 +219,17 @@ class Spyc {
* @return string
* @param $value The string you wish to fold
*/
- private function _doFolding($value,$indent) {
+ private function _doFolding( $value, $indent ) {
// Don't do anything if wordwrap is set to 0
- if ($this->_dumpWordWrap === 0) {
+ if ( $this->_dumpWordWrap === 0 ) {
return $value;
}
- if (strlen($value) > $this->_dumpWordWrap) {
+ if ( strlen( $value ) > $this->_dumpWordWrap ) {
$indent += $this->_dumpIndent;
- $indent = str_repeat(' ',$indent);
- $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent");
- $value = ">\n".$indent.$wrapped;
+ $indent = str_repeat( ' ', $indent );
+ $wrapped = wordwrap( $value, $this->_dumpWordWrap, "\n$indent" );
+ $value = ">-\n" . $indent . $wrapped;
}
return $value;
}
diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php
index c001a7dc..1f32e019 100644
--- a/includes/api/ApiHelp.php
+++ b/includes/api/ApiHelp.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -35,15 +35,15 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiHelp extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
/**
* Stub module for displaying help when no parameters are given
*/
public function execute() {
- $this->dieUsage('', 'help');
+ $this->dieUsage( '', 'help' );
}
public function shouldCheckMaxlag() {
@@ -61,6 +61,6 @@ class ApiHelp extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiHelp.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiHelp.php 60930 2010-01-11 15:55:52Z simetrical $';
}
}
diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php
index 4b1518bb..032b684c 100644
--- a/includes/api/ApiImport.php
+++ b/includes/api/ApiImport.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -35,70 +35,68 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiImport extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
global $wgUser;
- if(!$wgUser->isAllowed('import'))
- $this->dieUsageMsg(array('cantimport'));
+ if ( !$wgUser->isAllowed( 'import' ) )
+ $this->dieUsageMsg( array( 'cantimport' ) );
$params = $this->extractRequestParams();
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
$source = null;
$isUpload = false;
- if(isset($params['interwikisource']))
+ if ( isset( $params['interwikisource'] ) )
{
- if(!isset($params['interwikipage']))
- $this->dieUsageMsg(array('missingparam', 'interwikipage'));
+ if ( !isset( $params['interwikipage'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'interwikipage' ) );
$source = ImportStreamSource::newFromInterwiki(
$params['interwikisource'],
$params['interwikipage'],
$params['fullhistory'],
- $params['templates']);
+ $params['templates'] );
}
else
{
$isUpload = true;
- if(!$wgUser->isAllowed('importupload'))
- $this->dieUsageMsg(array('cantimport-upload'));
- $source = ImportStreamSource::newFromUpload('xml');
+ if ( !$wgUser->isAllowed( 'importupload' ) )
+ $this->dieUsageMsg( array( 'cantimport-upload' ) );
+ $source = ImportStreamSource::newFromUpload( 'xml' );
}
- if($source instanceof WikiErrorMsg)
- $this->dieUsageMsg(array_merge(
- array($source->getMessageKey()),
- $source->getMessageArgs()));
- else if(WikiError::isError($source))
+ if ( $source instanceof WikiErrorMsg )
+ $this->dieUsageMsg( array_merge(
+ array( $source->getMessageKey() ),
+ $source->getMessageArgs() ) );
+ else if ( WikiError::isError( $source ) )
// This shouldn't happen
- $this->dieUsageMsg(array('import-unknownerror', $source->getMessage()));
+ $this->dieUsageMsg( array( 'import-unknownerror', $source->getMessage() ) );
- $importer = new WikiImporter($source);
- if(isset($params['namespace']))
- $importer->setTargetNamespace($params['namespace']);
- $reporter = new ApiImportReporter($importer, $isUpload,
+ $importer = new WikiImporter( $source );
+ if ( isset( $params['namespace'] ) )
+ $importer->setTargetNamespace( $params['namespace'] );
+ $reporter = new ApiImportReporter( $importer, $isUpload,
$params['interwikisource'],
- $params['summary']);
+ $params['summary'] );
$result = $importer->doImport();
- if($result instanceof WikiXmlError)
- $this->dieUsageMsg(array('import-xml-error',
+ if ( $result instanceof WikiXmlError )
+ $this->dieUsageMsg( array( 'import-xml-error',
$result->mLine,
$result->mColumn,
$result->mByte . $result->mContext,
- xml_error_string($result->mXmlError)));
- else if(WikiError::isError($result))
- // This shouldn't happen
- $this->dieUsageMsg(array('import-unknownerror', $result->getMessage()));
+ xml_error_string( $result->mXmlError ) ) );
+ else if ( WikiError::isError( $result ) )
+ $this->dieUsageMsg( array( 'import-unknownerror', $result->getMessage() ) ); // This shouldn't happen
+
$resultData = $reporter->getData();
- $this->getResult()->setIndexedTagName($resultData, 'page');
- $this->getResult()->addValue(null, $this->getModuleName(), $resultData);
+ $this->getResult()->setIndexedTagName( $resultData, 'page' );
+ $this->getResult()->addValue( null, $this->getModuleName(), $resultData );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
@@ -140,6 +138,20 @@ class ApiImport extends ApiBase {
'Import a page from another wiki, or an XML file'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'cantimport' ),
+ array( 'missingparam', 'interwikipage' ),
+ array( 'cantimport-upload' ),
+ array( 'import-unknownerror', 'source' ),
+ array( 'import-unknownerror', 'result' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array(
@@ -149,7 +161,7 @@ class ApiImport extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiImport.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiImport.php 62599 2010-02-16 21:59:16Z reedy $';
}
}
@@ -160,20 +172,20 @@ class ApiImport extends ApiBase {
class ApiImportReporter extends ImportReporter {
private $mResultArr = array();
- function reportPage($title, $origTitle, $revisionCount, $successCount)
+ function reportPage( $title, $origTitle, $revisionCount, $successCount )
{
// Add a result entry
$r = array();
- ApiQueryBase::addTitleInfo($r, $title);
- $r['revisions'] = intval($successCount);
+ ApiQueryBase::addTitleInfo( $r, $title );
+ $r['revisions'] = intval( $successCount );
$this->mResultArr[] = $r;
// Piggyback on the parent to do the logging
- parent::reportPage($title, $origTitle, $revisionCount, $successCount);
+ parent::reportPage( $title, $origTitle, $revisionCount, $successCount );
}
function getData()
{
return $this->mResultArr;
}
-}
+} \ No newline at end of file
diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php
index 8f1fb834..442bc44c 100644
--- a/includes/api/ApiLogin.php
+++ b/includes/api/ApiLogin.php
@@ -1,11 +1,11 @@
<?php
-/*
+/**
* Created on Sep 19, 2006
*
* API for MediaWiki 1.8+
*
- * Copyright (C) 2006-2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
+ * Copyright © 2006-2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com,
* Daniel Cannon (cannon dot danielc at gmail dot com)
*
* This program is free software; you can redistribute it and/or modify
@@ -24,9 +24,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once( 'ApiBase.php' );
}
/**
@@ -36,8 +36,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiLogin extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action, 'lg');
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action, 'lg' );
}
/**
@@ -48,42 +48,40 @@ class ApiLogin extends ApiBase {
* user, or any other reason, the host is cached with an expiry
* and no log-in attempts will be accepted until that expiry
* is reached. The expiry is $this->mLoginThrottle.
- *
- * @access public
*/
public function execute() {
$params = $this->extractRequestParams();
- $result = array ();
+ $result = array();
- $req = new FauxRequest(array (
+ $req = new FauxRequest( array(
'wpName' => $params['name'],
'wpPassword' => $params['password'],
'wpDomain' => $params['domain'],
'wpLoginToken' => $params['token'],
'wpRemember' => ''
- ));
+ ) );
// Init session if necessary
- if( session_id() == '' ) {
+ if ( session_id() == '' ) {
wfSetupSession();
}
- $loginForm = new LoginForm($req);
- switch ($authRes = $loginForm->authenticateUserData()) {
- case LoginForm :: SUCCESS :
+ $loginForm = new LoginForm( $req );
+ switch ( $authRes = $loginForm->authenticateUserData() ) {
+ case LoginForm::SUCCESS:
global $wgUser, $wgCookiePrefix;
- $wgUser->setOption('rememberpassword', 1);
+ $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));
+ wfRunHooks( 'UserLoginComplete', array( &$wgUser, &$injected_html ) );
$result['result'] = 'Success';
- $result['lguserid'] = intval($wgUser->getId());
+ $result['lguserid'] = intval( $wgUser->getId() );
$result['lgusername'] = $wgUser->getName();
$result['lgtoken'] = $wgUser->getToken();
$result['cookieprefix'] = $wgCookiePrefix;
@@ -102,48 +100,63 @@ class ApiLogin extends ApiBase {
$result['result'] = 'WrongToken';
break;
- case LoginForm :: NO_NAME :
+ case LoginForm::NO_NAME:
$result['result'] = 'NoName';
break;
- case LoginForm :: ILLEGAL :
+
+ case LoginForm::ILLEGAL:
$result['result'] = 'Illegal';
break;
- case LoginForm :: WRONG_PLUGIN_PASS :
+
+ case LoginForm::WRONG_PLUGIN_PASS:
$result['result'] = 'WrongPluginPass';
break;
- case LoginForm :: NOT_EXISTS :
+
+ case LoginForm::NOT_EXISTS:
$result['result'] = 'NotExists';
break;
- case LoginForm :: WRONG_PASS :
+
+ case LoginForm::RESET_PASS: // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin - "The e-mailed temporary password should not be used for actual logins;"
+ case LoginForm::WRONG_PASS:
$result['result'] = 'WrongPass';
break;
- case LoginForm :: EMPTY_PASS :
+
+ case LoginForm::EMPTY_PASS:
$result['result'] = 'EmptyPass';
break;
- case LoginForm :: CREATE_BLOCKED :
+
+ case LoginForm::CREATE_BLOCKED:
$result['result'] = 'CreateBlocked';
$result['details'] = 'Your IP address is blocked from account creation';
break;
- case LoginForm :: THROTTLED :
+
+ case LoginForm::THROTTLED:
global $wgPasswordAttemptThrottle;
$result['result'] = 'Throttled';
- $result['wait'] = intval($wgPasswordAttemptThrottle['seconds']);
+ $result['wait'] = intval( $wgPasswordAttemptThrottle['seconds'] );
break;
- default :
- ApiBase :: dieDebug(__METHOD__, "Unhandled case value: {$authRes}");
+
+ case LoginForm::USER_BLOCKED:
+ $result['result'] = 'Blocked';
+ break;
+
+ default:
+ ApiBase::dieDebug( __METHOD__, "Unhandled case value: {$authRes}" );
}
- $this->getResult()->addValue(null, 'login', $result);
+ $this->getResult()->addValue( null, 'login', $result );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isReadMode() {
return false;
}
public function getAllowedParams() {
- return array (
+ return array(
'name' => null,
'password' => null,
'domain' => null,
@@ -152,7 +165,7 @@ class ApiLogin extends ApiBase {
}
public function getParamDescription() {
- return array (
+ return array(
'name' => 'User Name',
'password' => 'Password',
'domain' => 'Domain (optional)',
@@ -161,7 +174,7 @@ class ApiLogin extends ApiBase {
}
public function getDescription() {
- return array (
+ return array(
'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 ',
@@ -170,6 +183,22 @@ class ApiLogin extends ApiBase {
);
}
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'NeedToken', 'info' => 'You need to resubmit your login with the specified token. See https://bugzilla.wikimedia.org/show_bug.cgi?id=23076' ),
+ array( 'code' => 'WrongToken', 'info' => 'You specified an invalid token' ),
+ array( 'code' => 'NoName', 'info' => 'You didn\'t set the lgname parameter' ),
+ array( 'code' => 'Illegal', 'info' => ' You provided an illegal username' ),
+ array( 'code' => 'NotExists', 'info' => ' The username you provided doesn\'t exist' ),
+ array( 'code' => 'EmptyPass', 'info' => ' You didn\'t set the lgpassword parameter or you left it empty' ),
+ array( 'code' => 'WrongPass', 'info' => ' The password you provided is incorrect' ),
+ array( 'code' => 'WrongPluginPass', 'info' => 'Same as `WrongPass", returned when an authentication plugin rather than MediaWiki itself rejected the password' ),
+ array( 'code' => 'CreateBlocked', 'info' => 'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation' ),
+ array( 'code' => 'Throttled', 'info' => 'You\'ve logged in too many times in a short time' ),
+ array( 'code' => 'Blocked', 'info' => 'User is blocked' ),
+ ) );
+ }
+
protected function getExamples() {
return array(
'api.php?action=login&lgname=user&lgpassword=password'
@@ -177,6 +206,6 @@ class ApiLogin extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogin.php 69990 2010-07-27 08:44:08Z tstarling $';
+ return __CLASS__ . ': $Id: ApiLogin.php 64697 2010-04-07 09:05:05Z catrope $';
}
}
diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php
index aa9f2829..6637ee09 100644
--- a/includes/api/ApiLogout.php
+++ b/includes/api/ApiLogout.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -36,8 +36,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiLogout extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
@@ -47,7 +47,7 @@ class ApiLogout extends ApiBase {
// Give extensions to do something after user logout
$injected_html = '';
- wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) );
+ wfRunHooks( 'UserLogoutComplete', array( &$wgUser, &$injected_html, $oldName ) );
}
public function isReadMode() {
@@ -75,6 +75,6 @@ class ApiLogout extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiLogout.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiLogout.php 69578 2010-07-20 02:46:20Z tstarling $';
}
}
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 063e3574..fa6957b6 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -76,10 +76,12 @@ class ApiMain extends ApiBase {
'unblock' => 'ApiUnblock',
'move' => 'ApiMove',
'edit' => 'ApiEditPage',
+ 'upload' => 'ApiUpload',
'emailuser' => 'ApiEmailUser',
'watch' => 'ApiWatch',
'patrol' => 'ApiPatrol',
'import' => 'ApiImport',
+ 'userrights' => 'ApiUserrights',
);
/**
@@ -102,26 +104,28 @@ class ApiMain extends ApiBase {
'dbg' => 'ApiFormatDbg',
'dbgfm' => 'ApiFormatDbg'
);
-
+
/**
* List of user roles that are specifically relevant to the API.
* array( 'right' => array ( 'msg' => 'Some message with a $1',
* 'params' => array ( $someVarToSubst ) ),
* );
*/
- private static $mRights = array('writeapi' => array(
+ private static $mRights = array( 'writeapi' => array(
'msg' => 'Use of the write API',
'params' => array()
),
'apihighlimits' => array(
'msg' => 'Use higher limits in API queries (Slow queries: $1 results; Fast queries: $2 results). The limits for slow queries also apply to multivalue parameters.',
- 'params' => array (ApiMain::LIMIT_SML2, ApiMain::LIMIT_BIG2)
+ 'params' => array ( ApiMain::LIMIT_SML2, ApiMain::LIMIT_BIG2 )
)
);
private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames;
- private $mResult, $mAction, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode;
+ private $mResult, $mAction, $mShowVersions, $mEnableWrite, $mRequest;
+ private $mInternalMode, $mSquidMaxage, $mModule;
+
private $mCacheMode = 'private';
private $mCacheControl = array();
@@ -131,21 +135,21 @@ class ApiMain extends ApiBase {
* @param $request object - if this is an instance of FauxRequest, errors are thrown and no printing occurs
* @param $enableWrite bool should be set to true if the api may modify data
*/
- public function __construct($request, $enableWrite = false) {
+ public function __construct( $request, $enableWrite = false ) {
- $this->mInternalMode = ($request instanceof FauxRequest);
+ $this->mInternalMode = ( $request instanceof FauxRequest );
// Special handling for the main module: $parent === $this
- parent :: __construct($this, $this->mInternalMode ? 'main_int' : 'main');
+ parent :: __construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
- if (!$this->mInternalMode) {
+ if ( !$this->mInternalMode ) {
// Impose module restrictions.
// If the current user cannot read,
// Remove all modules other than login
global $wgUser;
- if( $request->getVal( 'callback' ) !== null ) {
+ 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" );
@@ -156,16 +160,17 @@ class ApiMain extends ApiBase {
global $wgAPIModules; // extension modules
$this->mModules = $wgAPIModules + self :: $Modules;
- $this->mModuleNames = array_keys($this->mModules);
+ $this->mModuleNames = array_keys( $this->mModules );
$this->mFormats = self :: $Formats;
- $this->mFormatNames = array_keys($this->mFormats);
+ $this->mFormatNames = array_keys( $this->mFormats );
- $this->mResult = new ApiResult($this);
+ $this->mResult = new ApiResult( $this );
$this->mShowVersions = false;
$this->mEnableWrite = $enableWrite;
$this->mRequest = & $request;
+ $this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling()
$this->mCommit = false;
}
@@ -184,27 +189,34 @@ class ApiMain extends ApiBase {
}
/**
- * Get the ApiResult object asscosiated with current request
+ * Get the ApiResult object associated with current request
*/
public function getResult() {
return $this->mResult;
}
/**
+ * Get the API module object. Only works after executeAction()
+ */
+ public function getModule() {
+ return $this->mModule;
+ }
+
+ /**
* Only kept for backwards compatibility
* @deprecated Use isWriteMode() instead
*/
public function requestWriteMode() {
- if (!$this->mEnableWrite)
- $this->dieUsageMsg(array('writedisabled'));
- if (wfReadOnly())
- $this->dieUsageMsg(array('readonlytext'));
+ if ( !$this->mEnableWrite )
+ $this->dieUsageMsg( array( 'writedisabled' ) );
+ if ( wfReadOnly() )
+ $this->dieUsageMsg( array( 'readonlytext' ) );
}
/**
* Set how long the response should be cached.
*/
- public function setCacheMaxAge($maxage) {
+ public function setCacheMaxAge( $maxage ) {
$this->setCacheControl( array(
'max-age' => $maxage,
's-maxage' => $maxage
@@ -293,10 +305,10 @@ class ApiMain extends ApiBase {
/**
* Create an instance of an output formatter by its name
*/
- public function createPrinterByName($format) {
- if( !isset( $this->mFormats[$format] ) )
+ public function createPrinterByName( $format ) {
+ if ( !isset( $this->mFormats[$format] ) )
$this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
- return new $this->mFormats[$format] ($this, $format);
+ return new $this->mFormats[$format] ( $this, $format );
}
/**
@@ -304,11 +316,11 @@ class ApiMain extends ApiBase {
*/
public function execute() {
$this->profileIn();
- if ($this->mInternalMode)
+ if ( $this->mInternalMode )
$this->executeAction();
else
$this->executeActionWithErrorHandling();
-
+
$this->profileOut();
}
@@ -324,7 +336,7 @@ class ApiMain extends ApiBase {
try {
$this->executeAction();
- } catch (Exception $e) {
+ } catch ( Exception $e ) {
// Log it
if ( $e instanceof MWException ) {
wfDebugLog( 'exception', $e->getLogMessage() );
@@ -336,31 +348,32 @@ class ApiMain extends ApiBase {
// handler will process and log it.
//
- $errCode = $this->substituteResultWithError($e);
+ $errCode = $this->substituteResultWithError( $e );
// Error results should not be cached
$this->setCacheMode( 'private' );
$headerStr = 'MediaWiki-API-Error: ' . $errCode;
- if ($e->getCode() === 0)
- header($headerStr);
+ if ( $e->getCode() === 0 )
+ header( $headerStr );
else
- header($headerStr, true, $e->getCode());
+ header( $headerStr, true, $e->getCode() );
// Reset and print just the error message
ob_clean();
// If the error occured during printing, do a printer->profileOut()
$this->mPrinter->safeProfileOut();
- $this->printResult(true);
+ $this->printResult( true );
}
// Send cache headers after any code which might generate an error, to
// avoid sending public cache headers for errors.
$this->sendCacheHeaders();
- if($this->mPrinter->getIsHtml())
+ if ( $this->mPrinter->getIsHtml() ) {
echo wfReportTime();
+ }
ob_end_flush();
}
@@ -372,15 +385,22 @@ class ApiMain extends ApiBase {
}
if ( $this->mCacheMode == 'anon-public-user-private' ) {
- global $wgOut;
+ global $wgUseXVO, $wgOut;
header( 'Vary: Accept-Encoding, Cookie' );
- header( $wgOut->getXVO() );
- if ( session_id() != '' || $wgOut->haveCacheVaryCookies() ) {
- // Logged in, mark this request private
+ if ( $wgUseXVO ) {
+ header( $wgOut->getXVO() );
+ if ( $wgOut->haveCacheVaryCookies() ) {
+ // Logged in, mark this request private
+ header( 'Cache-Control: private' );
+ return;
+ }
+ // Logged out, send normal public headers below
+ } elseif ( session_id() != '' ) {
+ // Logged in or otherwise has session (e.g. anonymous users who have edited)
+ // Mark request private
header( 'Cache-Control: private' );
return;
- }
- // Logged out, send normal public headers below
+ } // else no XVO and anonymous, send public headers below
} else /* if public */ {
// Give a debugging message if the user object is unstubbed on a public request
global $wgUser;
@@ -396,7 +416,7 @@ class ApiMain extends ApiBase {
if ( !isset( $this->mCacheControl['max-age'] ) ) {
$this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
}
-
+
if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
// Public cache not requested
// Sending a Vary header in this case is harmless, and protects us
@@ -426,7 +446,7 @@ class ApiMain extends ApiBase {
$separator = ', ';
}
}
-
+
header( "Cache-Control: $ccHeader" );
}
@@ -434,57 +454,55 @@ class ApiMain extends ApiBase {
* Replace the result data with the information about an exception.
* Returns the error code
*/
- protected function substituteResultWithError($e) {
+ 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.
- $value = $this->getRequest()->getVal('format', self::API_DEFAULT_FORMAT);
- if (!in_array($value, $this->mFormatNames))
- $value = self::API_DEFAULT_FORMAT;
+ // 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.
+ $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
+ if ( !in_array( $value, $this->mFormatNames ) )
+ $value = self::API_DEFAULT_FORMAT;
- $this->mPrinter = $this->createPrinterByName($value);
- if ($this->mPrinter->getNeedsRawData())
- $this->getResult()->setRawMode();
- }
+ $this->mPrinter = $this->createPrinterByName( $value );
+ if ( $this->mPrinter->getNeedsRawData() )
+ $this->getResult()->setRawMode();
+ }
- if ($e instanceof UsageException) {
- //
- // User entered incorrect parameters - print usage screen
- //
- $errMessage = array (
- 'code' => $e->getCodeString(),
- 'info' => $e->getMessage());
+ if ( $e instanceof UsageException ) {
+ //
+ // User entered incorrect parameters - print usage screen
+ //
+ $errMessage = $e->getMessageArray();
- // 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());
+ // Only print the help message when this is for the developer, not runtime
+ if ( $this->mPrinter->getWantsHelp() || $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 {
- 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' => $info,
- );
- ApiResult :: setContent($errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : "" );
+ $info = "Exception Caught: {$e->getMessage()}";
}
- $this->getResult()->reset();
- $this->getResult()->disableSizeCheck();
- // Re-add the id
- $requestid = $this->getParameter('requestid');
- if(!is_null($requestid))
- $this->getResult()->addValue(null, 'requestid', $requestid);
- $this->getResult()->addValue(null, 'error', $errMessage);
+ $errMessage = array (
+ 'code' => 'internal_api_error_' . get_class( $e ),
+ 'info' => $info,
+ );
+ ApiResult :: setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : "" );
+ }
+
+ $this->getResult()->reset();
+ $this->getResult()->disableSizeCheck();
+ // Re-add the id
+ $requestid = $this->getParameter( 'requestid' );
+ if ( !is_null( $requestid ) )
+ $this->getResult()->addValue( null, 'requestid', $requestid );
+ $this->getResult()->addValue( null, 'error', $errMessage );
return $errMessage['code'];
}
@@ -494,23 +512,40 @@ class ApiMain extends ApiBase {
*/
protected function executeAction() {
// First add the id to the top element
- $requestid = $this->getParameter('requestid');
- if(!is_null($requestid))
- $this->getResult()->addValue(null, 'requestid', $requestid);
+ $requestid = $this->getParameter( 'requestid' );
+ if ( !is_null( $requestid ) )
+ $this->getResult()->addValue( null, 'requestid', $requestid );
$params = $this->extractRequestParams();
$this->mShowVersions = $params['version'];
$this->mAction = $params['action'];
- if( !is_string( $this->mAction ) ) {
+ if ( !is_string( $this->mAction ) ) {
$this->dieUsage( "The API requires a valid action parameter", 'unknown_action' );
}
-
+
// Instantiate the module requested by the user
- $module = new $this->mModules[$this->mAction] ($this, $this->mAction);
+ $module = new $this->mModules[$this->mAction] ( $this, $this->mAction );
+ $this->mModule = $module;
+
+ $moduleParams = $module->extractRequestParams();
+
+ // Die if token required, but not provided (unless there is a gettoken parameter)
+ $salt = $module->getTokenSalt();
+ if ( $salt !== false && !isset( $moduleParams['gettoken'] ) )
+ {
+ if ( !isset( $moduleParams['token'] ) ) {
+ $this->dieUsageMsg( array( 'missingparam', 'token' ) );
+ } else {
+ global $wgUser;
+ if ( !$wgUser->matchEditToken( $moduleParams['token'], $salt ) ) {
+ $this->dieUsageMsg( array( 'sessionfailure' ) );
+ }
+ }
+ }
- if( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
+ if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
// Check for maxlag
global $wgShowHostnames;
$maxLag = $params['maxlag'];
@@ -518,8 +553,7 @@ class ApiMain extends ApiBase {
if ( $lag > $maxLag ) {
header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
header( 'X-Database-Lag: ' . intval( $lag ) );
- // XXX: should we return a 503 HTTP error code like wfMaxlagError() does?
- if( $wgShowHostnames ) {
+ if ( $wgShowHostnames ) {
$this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
} else {
$this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
@@ -528,50 +562,50 @@ class ApiMain extends ApiBase {
}
}
- global $wgUser;
- if ($module->isReadMode() && !$wgUser->isAllowed('read'))
- $this->dieUsageMsg(array('readrequired'));
- if ($module->isWriteMode()) {
- if (!$this->mEnableWrite)
- $this->dieUsageMsg(array('writedisabled'));
- if (!$wgUser->isAllowed('writeapi'))
- $this->dieUsageMsg(array('writerequired'));
- if (wfReadOnly())
- $this->dieUsageMsg(array('readonlytext'));
+ global $wgUser, $wgGroupPermissions;
+ if ( $module->isReadMode() && !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) && !$wgUser->isAllowed( 'read' ) )
+ $this->dieUsageMsg( array( 'readrequired' ) );
+ if ( $module->isWriteMode() ) {
+ if ( !$this->mEnableWrite )
+ $this->dieUsageMsg( array( 'writedisabled' ) );
+ if ( !$wgUser->isAllowed( 'writeapi' ) )
+ $this->dieUsageMsg( array( 'writerequired' ) );
+ if ( wfReadOnly() )
+ $this->dieReadOnly();
}
- if (!$this->mInternalMode) {
+ if ( !$this->mInternalMode ) {
// Ignore mustBePosted() for internal calls
- if($module->mustBePosted() && !$this->mRequest->wasPosted())
- $this->dieUsage("The {$this->mAction} module requires a POST request", 'mustbeposted');
+ if ( $module->mustBePosted() && !$this->mRequest->wasPosted() )
+ $this->dieUsageMsg( array ( 'mustbeposted', $this->mAction ) );
// See if custom printer is used
$this->mPrinter = $module->getCustomPrinter();
- if (is_null($this->mPrinter)) {
+ if ( is_null( $this->mPrinter ) ) {
// Create an appropriate printer
- $this->mPrinter = $this->createPrinterByName($params['format']);
+ $this->mPrinter = $this->createPrinterByName( $params['format'] );
}
- if ($this->mPrinter->getNeedsRawData())
+ if ( $this->mPrinter->getNeedsRawData() )
$this->getResult()->setRawMode();
}
// Execute
$module->profileIn();
$module->execute();
- wfRunHooks('APIAfterExecute', array(&$module));
+ wfRunHooks( 'APIAfterExecute', array( &$module ) );
$module->profileOut();
- if (!$this->mInternalMode) {
+ if ( !$this->mInternalMode ) {
// Print result data
- $this->printResult(false);
+ $this->printResult( false );
}
}
/**
* Print results using the current printer
*/
- protected function printResult($isError) {
+ protected function printResult( $isError ) {
$this->getResult()->cleanUpUTF8();
$printer = $this->mPrinter;
$printer->profileIn();
@@ -582,13 +616,13 @@ class ApiMain extends ApiBase {
$printer->setUnescapeAmps ( ( $this->mAction == 'help' || $isError )
&& $printer->getFormat() == 'XML' && $printer->getIsHtml() );
- $printer->initPrinter($isError);
+ $printer->initPrinter( $isError );
$printer->execute();
$printer->closePrinter();
$printer->profileOut();
}
-
+
public function isReadMode() {
return false;
}
@@ -668,6 +702,16 @@ class ApiMain extends ApiBase {
);
}
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'readonlytext' ),
+ array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ),
+ array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ),
+ array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ),
+ array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ),
+ ) );
+ }
+
/**
* Returns an array of strings with credits for the API
*/
@@ -677,6 +721,7 @@ class ApiMain extends ApiBase {
' Roan Kattouw <Firstname>.<Lastname>@home.nl (lead developer Sep 2007-present)',
' Victor Vasiliev - vasilvv at gee mail dot com',
' Bryan Tong Minh - bryan . tongminh @ gmail . com',
+ ' Sam Reed - sam @ reedyboy . net',
' 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',
@@ -688,19 +733,37 @@ class ApiMain extends ApiBase {
* Override the parent to generate help messages for all available modules.
*/
public function makeHelpMsg() {
+ global $wgMemc, $wgAPICacheHelp, $wgAPICacheHelpTimeout;
+ $this->mPrinter->setHelp();
+ // Get help text from cache if present
+ $key = wfMemcKey( 'apihelp', $this->getModuleName(),
+ SpecialVersion::getVersion( 'nodb' ) .
+ $this->getMain()->getShowVersions() );
+ if ( $wgAPICacheHelp ) {
+ $cached = $wgMemc->get( $key );
+ if ( $cached )
+ return $cached;
+ }
+ $retval = $this->reallyMakeHelpMsg();
+ if ( $wgAPICacheHelp )
+ $wgMemc->set( $key, $retval, $wgAPICacheHelpTimeout );
+ return $retval;
+ }
+
+ public function reallyMakeHelpMsg() {
$this->mPrinter->setHelp();
// Use parent to make default message for the main module
$msg = parent :: makeHelpMsg();
- $astriks = str_repeat('*** ', 10);
+ $astriks = str_repeat( '*** ', 10 );
$msg .= "\n\n$astriks Modules $astriks\n\n";
- foreach( $this->mModules as $moduleName => $unused ) {
- $module = new $this->mModules[$moduleName] ($this, $moduleName);
- $msg .= self::makeHelpMsgHeader($module, 'action');
+ foreach ( $this->mModules as $moduleName => $unused ) {
+ $module = new $this->mModules[$moduleName] ( $this, $moduleName );
+ $msg .= self::makeHelpMsgHeader( $module, 'action' );
$msg2 = $module->makeHelpMsg();
- if ($msg2 !== false)
+ if ( $msg2 !== false )
$msg .= $msg2;
$msg .= "\n";
}
@@ -708,30 +771,30 @@ class ApiMain extends ApiBase {
$msg .= "\n$astriks Permissions $astriks\n\n";
foreach ( self :: $mRights as $right => $rightMsg ) {
$groups = User::getGroupsWithPermission( $right );
- $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg[ 'msg' ], $rightMsg[ 'params' ] ) .
+ $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg[ 'msg' ], $rightMsg[ 'params' ] ) .
"\nGranted to:\n " . str_replace( "*", "all", implode( ", ", $groups ) ) . "\n";
}
$msg .= "\n$astriks Formats $astriks\n\n";
- foreach( $this->mFormats as $formatName => $unused ) {
- $module = $this->createPrinterByName($formatName);
- $msg .= self::makeHelpMsgHeader($module, 'format');
+ foreach ( $this->mFormats as $formatName => $unused ) {
+ $module = $this->createPrinterByName( $formatName );
+ $msg .= self::makeHelpMsgHeader( $module, 'format' );
$msg2 = $module->makeHelpMsg();
- if ($msg2 !== false)
+ if ( $msg2 !== false )
$msg .= $msg2;
$msg .= "\n";
}
- $msg .= "\n*** Credits: ***\n " . implode("\n ", $this->getCredits()) . "\n";
+ $msg .= "\n*** Credits: ***\n " . implode( "\n ", $this->getCredits() ) . "\n";
return $msg;
}
- public static function makeHelpMsgHeader($module, $paramName) {
+ public static function makeHelpMsgHeader( $module, $paramName ) {
$modulePrefix = $module->getModulePrefix();
- if (strval($modulePrefix) !== '')
+ if ( strval( $modulePrefix ) !== '' )
$modulePrefix = "($modulePrefix) ";
return "* $paramName={$module->getModuleName()} $modulePrefix*";
@@ -746,9 +809,9 @@ class ApiMain extends ApiBase {
* OBSOLETE, use canApiHighLimits() instead
*/
public function isBot() {
- if (!isset ($this->mIsBot)) {
+ if ( !isset ( $this->mIsBot ) ) {
global $wgUser;
- $this->mIsBot = $wgUser->isAllowed('bot');
+ $this->mIsBot = $wgUser->isAllowed( 'bot' );
}
return $this->mIsBot;
}
@@ -759,9 +822,9 @@ class ApiMain extends ApiBase {
* OBSOLETE, use canApiHighLimits() instead
*/
public function isSysop() {
- if (!isset ($this->mIsSysop)) {
+ if ( !isset ( $this->mIsSysop ) ) {
global $wgUser;
- $this->mIsSysop = in_array( 'sysop', $wgUser->getGroups());
+ $this->mIsSysop = in_array( 'sysop', $wgUser->getGroups() );
}
return $this->mIsSysop;
@@ -772,9 +835,9 @@ class ApiMain extends ApiBase {
* @return bool
*/
public function canApiHighLimits() {
- if (!isset($this->mCanApiHighLimits)) {
+ if ( !isset( $this->mCanApiHighLimits ) ) {
global $wgUser;
- $this->mCanApiHighLimits = $wgUser->isAllowed('apihighlimits');
+ $this->mCanApiHighLimits = $wgUser->isAllowed( 'apihighlimits' );
}
return $this->mCanApiHighLimits;
@@ -795,11 +858,10 @@ class ApiMain extends ApiBase {
public function getVersion() {
$vers = array ();
$vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/";
- $vers[] = __CLASS__ . ': $Id: ApiMain.php 69990 2010-07-27 08:44:08Z tstarling $';
+ $vers[] = __CLASS__ . ': $Id: ApiMain.php 70066 2010-07-28 05:52:32Z tstarling $';
$vers[] = ApiBase :: getBaseVersion();
$vers[] = ApiFormatBase :: getBaseVersion();
$vers[] = ApiQueryBase :: getBaseVersion();
- $vers[] = ApiFormatFeedWrapper :: getVersion(); // not accessible with format=xxx
return $vers;
}
@@ -845,14 +907,25 @@ class ApiMain extends ApiBase {
class UsageException extends Exception {
private $mCodestr;
+ private $mExtraData;
- public function __construct($message, $codestr, $code = 0) {
- parent :: __construct($message, $code);
+ public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
+ parent :: __construct( $message, $code );
$this->mCodestr = $codestr;
+ $this->mExtraData = $extradata;
}
public function getCodeString() {
return $this->mCodestr;
}
+ public function getMessageArray() {
+ $result = array (
+ 'code' => $this->mCodestr,
+ 'info' => $this->getMessage()
+ );
+ if ( is_array( $this->mExtraData ) )
+ $result = array_merge( $result, $this->mExtraData );
+ return $result;
+ }
public function __toString() {
return "{$this->getCodeString()}: {$this->getMessage()}";
}
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index e22d0294..71010de7 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
@@ -33,60 +33,68 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiMove extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
global $wgUser;
$params = $this->extractRequestParams();
- if(is_null($params['reason']))
+ if ( is_null( $params['reason'] ) )
$params['reason'] = '';
- $this->requireOnlyOneParameter($params, 'from', 'fromid');
- if(!isset($params['to']))
- $this->dieUsageMsg(array('missingparam', 'to'));
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
+ $this->requireOnlyOneParameter( $params, 'from', 'fromid' );
+ if ( !isset( $params['to'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'to' ) );
- if(isset($params['from']))
+ if ( isset( $params['from'] ) )
{
- $fromTitle = Title::newFromText($params['from']);
- if(!$fromTitle)
- $this->dieUsageMsg(array('invalidtitle', $params['from']));
+ $fromTitle = Title::newFromText( $params['from'] );
+ if ( !$fromTitle )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['from'] ) );
}
- else if(isset($params['fromid']))
+ else if ( isset( $params['fromid'] ) )
{
- $fromTitle = Title::newFromID($params['fromid']);
- if(!$fromTitle)
- $this->dieUsageMsg(array('nosuchpageid', $params['fromid']));
+ $fromTitle = Title::newFromID( $params['fromid'] );
+ if ( !$fromTitle )
+ $this->dieUsageMsg( array( 'nosuchpageid', $params['fromid'] ) );
}
- if(!$fromTitle->exists())
- $this->dieUsageMsg(array('notanarticle'));
+
+ if ( !$fromTitle->exists() )
+ $this->dieUsageMsg( array( 'notanarticle' ) );
$fromTalk = $fromTitle->getTalkPage();
- $toTitle = Title::newFromText($params['to']);
- if(!$toTitle)
- $this->dieUsageMsg(array('invalidtitle', $params['to']));
+ $toTitle = Title::newFromText( $params['to'] );
+ if ( !$toTitle )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['to'] ) );
$toTalk = $toTitle->getTalkPage();
- # Move the page
+ if ( $toTitle->getNamespace() == NS_FILE
+ && !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle )
+ && wfFindFile( $toTitle ) )
+ {
+ if ( !$params['ignorewarnings'] && $wgUser->isAllowed( 'reupload-shared' ) ) {
+ $this->dieUsageMsg( array( 'sharedfile-exists' ) );
+ } elseif ( !$wgUser->isAllowed( 'reupload-shared' ) ) {
+ $this->dieUsageMsg( array( 'cantoverwrite-sharedfile' ) );
+ }
+ }
+
+ // Move the page
$hookErr = null;
- $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']);
- if($retval !== true)
- $this->dieUsageMsg(reset($retval));
+ $retval = $fromTitle->moveTo( $toTitle, true, $params['reason'], !$params['noredirect'] );
+ if ( $retval !== true )
+ $this->dieUsageMsg( reset( $retval ) );
- $r = array('from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason']);
- if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect'))
+ $r = array( 'from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason'] );
+ if ( !$params['noredirect'] || !$wgUser->isAllowed( 'suppressredirect' ) )
$r['redirectcreated'] = '';
- # Move the talk page
- if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage())
+ // Move the talk page
+ if ( $params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage() )
{
- $retval = $fromTalk->moveTo($toTalk, true, $params['reason'], !$params['noredirect']);
- if($retval === true)
+ $retval = $fromTalk->moveTo( $toTalk, true, $params['reason'], !$params['noredirect'] );
+ if ( $retval === true )
{
$r['talkfrom'] = $fromTalk->getPrefixedText();
$r['talkto'] = $toTalk->getPrefixedText();
@@ -94,55 +102,55 @@ class ApiMove extends ApiBase {
// We're not gonna dieUsage() on failure, since we already changed something
else
{
- $parsed = $this->parseMsg(reset($retval));
+ $parsed = $this->parseMsg( reset( $retval ) );
$r['talkmove-error-code'] = $parsed['code'];
$r['talkmove-error-info'] = $parsed['info'];
}
}
- # Move subpages
- if($params['movesubpages'])
+ // Move subpages
+ if ( $params['movesubpages'] )
{
- $r['subpages'] = $this->moveSubpages($fromTitle, $toTitle,
- $params['reason'], $params['noredirect']);
- $this->getResult()->setIndexedTagName($r['subpages'], 'subpage');
- if($params['movetalk'])
+ $r['subpages'] = $this->moveSubpages( $fromTitle, $toTitle,
+ $params['reason'], $params['noredirect'] );
+ $this->getResult()->setIndexedTagName( $r['subpages'], 'subpage' );
+ if ( $params['movetalk'] )
{
- $r['subpages-talk'] = $this->moveSubpages($fromTalk, $toTalk,
- $params['reason'], $params['noredirect']);
- $this->getResult()->setIndexedTagName($r['subpages-talk'], 'subpage');
+ $r['subpages-talk'] = $this->moveSubpages( $fromTalk, $toTalk,
+ $params['reason'], $params['noredirect'] );
+ $this->getResult()->setIndexedTagName( $r['subpages-talk'], 'subpage' );
}
}
- # Watch pages
- if($params['watch'] || $wgUser->getOption('watchmoves'))
+ // Watch pages
+ if ( $params['watch'] || $wgUser->getOption( 'watchmoves' ) )
{
- $wgUser->addWatch($fromTitle);
- $wgUser->addWatch($toTitle);
+ $wgUser->addWatch( $fromTitle );
+ $wgUser->addWatch( $toTitle );
}
- else if($params['unwatch'])
+ else if ( $params['unwatch'] )
{
- $wgUser->removeWatch($fromTitle);
- $wgUser->removeWatch($toTitle);
+ $wgUser->removeWatch( $fromTitle );
+ $wgUser->removeWatch( $toTitle );
}
- $this->getResult()->addValue(null, $this->getModuleName(), $r);
+ $this->getResult()->addValue( null, $this->getModuleName(), $r );
}
-
- public function moveSubpages($fromTitle, $toTitle, $reason, $noredirect)
+
+ public function moveSubpages( $fromTitle, $toTitle, $reason, $noredirect )
{
$retval = array();
- $success = $fromTitle->moveSubpages($toTitle, true, $reason, !$noredirect);
- if(isset($success[0]))
- return array('error' => $this->parseMsg($success));
+ $success = $fromTitle->moveSubpages( $toTitle, true, $reason, !$noredirect );
+ if ( isset( $success[0] ) )
+ return array( 'error' => $this->parseMsg( $success ) );
else
{
// At least some pages could be moved
// Report each of them separately
- foreach($success as $oldTitle => $newTitle)
+ foreach ( $success as $oldTitle => $newTitle )
{
- $r = array('from' => $oldTitle);
- if(is_array($newTitle))
- $r['error'] = $this->parseMsg(reset($newTitle));
+ $r = array( 'from' => $oldTitle );
+ if ( is_array( $newTitle ) )
+ $r['error'] = $this->parseMsg( reset( $newTitle ) );
else
// Success
$r['to'] = $newTitle;
@@ -152,7 +160,9 @@ class ApiMove extends ApiBase {
return $retval;
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
@@ -171,7 +181,8 @@ class ApiMove extends ApiBase {
'movesubpages' => false,
'noredirect' => false,
'watch' => false,
- 'unwatch' => false
+ 'unwatch' => false,
+ 'ignorewarnings' => false
);
}
@@ -186,7 +197,8 @@ class ApiMove extends ApiBase {
'movesubpages' => 'Move subpages, if applicable',
'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'
+ 'unwatch' => 'Remove the page and the redirect from your watchlist',
+ 'ignorewarnings' => 'Ignore any warnings'
);
}
@@ -195,6 +207,21 @@ class ApiMove extends ApiBase {
'Move a page.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'to' ),
+ array( 'invalidtitle', 'from' ),
+ array( 'nosuchpageid', 'fromid' ),
+ array( 'notanarticle' ),
+ array( 'invalidtitle', 'to' ),
+ array( 'sharedfile-exists' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array (
@@ -203,6 +230,6 @@ class ApiMove extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiMove.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiMove.php 62810 2010-02-22 03:34:56Z mah $';
}
}
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index d2e6ea21..e145d80c 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -33,34 +33,38 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiOpenSearch extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function getCustomPrinter() {
- return $this->getMain()->createPrinterByName('json');
+ return $this->getMain()->createPrinterByName( 'json' );
}
public function execute() {
- global $wgEnableMWSuggest;
+ global $wgEnableOpenSearchSuggest, $wgSearchSuggestCacheExpiry;
$params = $this->extractRequestParams();
$search = $params['search'];
$limit = $params['limit'];
$namespaces = $params['namespace'];
$suggest = $params['suggest'];
- # $wgEnableMWSuggest hit incoming when $wgEnableMWSuggest is disabled
- if( $suggest && !$wgEnableMWSuggest ) return;
-
- // Open search results may be stored for a very long time
- $this->getMain()->setCacheMaxAge(1200);
- $this->getMain()->setCacheMode( 'public' );
- $srchres = PrefixSearch::titleSearch( $search, $limit, $namespaces );
+ // MWSuggest or similar hit
+ if ( $suggest && !$wgEnableOpenSearchSuggest )
+ $srchres = array();
+ else {
+ // Open search results may be stored for a very long
+ // time
+ $this->getMain()->setCacheMaxAge( $wgSearchSuggestCacheExpiry );
+ $this->getMain()->setCacheMode( 'public' );
+ $srchres = PrefixSearch::titleSearch( $search, $limit,
+ $namespaces );
+ }
// Set top level elements
$result = $this->getResult();
- $result->addValue(null, 0, $search);
- $result->addValue(null, 1, $srchres);
+ $result->addValue( null, 0, $search );
+ $result->addValue( null, 1, $srchres );
}
public function getAllowedParams() {
@@ -87,7 +91,7 @@ class ApiOpenSearch extends ApiBase {
'search' => 'Search string',
'limit' => 'Maximum amount of results to return',
'namespace' => 'Namespaces to search',
- 'suggest' => 'Do nothing if $wgEnableMWSuggest is false',
+ 'suggest' => 'Do nothing if $wgEnableOpenSearchSuggest is false',
);
}
@@ -102,6 +106,6 @@ class ApiOpenSearch extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiOpenSearch.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiOpenSearch.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index 6b9e90b8..361f1d8b 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -58,8 +58,8 @@ class ApiPageSet extends ApiQueryBase {
* @param $query ApiQuery
* @param $resolveRedirects bool Whether redirects should be resolved
*/
- public function __construct($query, $resolveRedirects = false) {
- parent :: __construct($query, 'query');
+ public function __construct( $query, $resolveRedirects = false ) {
+ parent :: __construct( $query, 'query' );
$this->mAllPages = array ();
$this->mTitles = array();
@@ -75,10 +75,10 @@ class ApiPageSet extends ApiQueryBase {
$this->mRequestedPageFields = array ();
$this->mResolveRedirects = $resolveRedirects;
- if($resolveRedirects)
+ if ( $resolveRedirects )
$this->mPendingRedirectIDs = array();
- $this->mFakePageId = -1;
+ $this->mFakePageId = - 1;
}
/**
@@ -94,7 +94,7 @@ class ApiPageSet extends ApiQueryBase {
* before execute()
* @param $fieldName string Field name
*/
- public function requestField($fieldName) {
+ public function requestField( $fieldName ) {
$this->mRequestedPageFields[$fieldName] = null;
}
@@ -104,7 +104,7 @@ class ApiPageSet extends ApiQueryBase {
* @param $fieldName string Field name
* @return mixed Field value
*/
- public function getCustomField($fieldName) {
+ public function getCustomField( $fieldName ) {
return $this->mRequestedPageFields[$fieldName];
}
@@ -123,14 +123,14 @@ class ApiPageSet extends ApiQueryBase {
'page_id' => null,
);
- if ($this->mResolveRedirects)
+ if ( $this->mResolveRedirects )
$pageFlds['page_is_redirect'] = null;
// only store non-default fields
- $this->mRequestedPageFields = array_diff_key($this->mRequestedPageFields, $pageFlds);
+ $this->mRequestedPageFields = array_diff_key( $this->mRequestedPageFields, $pageFlds );
- $pageFlds = array_merge($pageFlds, $this->mRequestedPageFields);
- return array_keys($pageFlds);
+ $pageFlds = array_merge( $pageFlds, $this->mRequestedPageFields );
+ return array_keys( $pageFlds );
}
/**
@@ -156,7 +156,7 @@ class ApiPageSet extends ApiQueryBase {
* @return int
*/
public function getTitleCount() {
- return count($this->mTitles);
+ return count( $this->mTitles );
}
/**
@@ -172,7 +172,7 @@ class ApiPageSet extends ApiQueryBase {
* @return int
*/
public function getGoodTitleCount() {
- return count($this->mGoodTitles);
+ return count( $this->mGoodTitles );
}
/**
@@ -249,7 +249,7 @@ class ApiPageSet extends ApiQueryBase {
* @return int
*/
public function getRevisionCount() {
- return count($this->getRevisionIDs());
+ return count( $this->getRevisionIDs() );
}
/**
@@ -261,32 +261,32 @@ class ApiPageSet extends ApiQueryBase {
// Only one of the titles/pageids/revids is allowed at the same time
$dataSource = null;
- if (isset ($params['titles']))
+ if ( isset ( $params['titles'] ) )
$dataSource = 'titles';
- if (isset ($params['pageids'])) {
- if (isset ($dataSource))
- $this->dieUsage("Cannot use 'pageids' at the same time as '$dataSource'", 'multisource');
+ if ( isset ( $params['pageids'] ) ) {
+ if ( isset ( $dataSource ) )
+ $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' );
$dataSource = 'pageids';
}
- if (isset ($params['revids'])) {
- if (isset ($dataSource))
- $this->dieUsage("Cannot use 'revids' at the same time as '$dataSource'", 'multisource');
+ if ( isset ( $params['revids'] ) ) {
+ if ( isset ( $dataSource ) )
+ $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' );
$dataSource = 'revids';
}
- switch ($dataSource) {
+ switch ( $dataSource ) {
case 'titles' :
- $this->initFromTitles($params['titles']);
+ $this->initFromTitles( $params['titles'] );
break;
case 'pageids' :
- $this->initFromPageIds($params['pageids']);
+ $this->initFromPageIds( $params['pageids'] );
break;
case 'revids' :
- if($this->mResolveRedirects)
- $this->setWarning('Redirect resolution cannot be used together with the revids= parameter. '.
- 'Any redirects the revids= point to have not been resolved.');
+ if ( $this->mResolveRedirects )
+ $this->setWarning( 'Redirect resolution cannot be used together with the revids= parameter. ' .
+ 'Any redirects the revids= point to have not been resolved.' );
$this->mResolveRedirects = false;
- $this->initFromRevIDs($params['revids']);
+ $this->initFromRevIDs( $params['revids'] );
break;
default :
// Do nothing - some queries do not need any of the data sources.
@@ -299,9 +299,9 @@ class ApiPageSet extends ApiQueryBase {
* Populate this PageSet from a list of Titles
* @param $titles array of Title objects
*/
- public function populateFromTitles($titles) {
+ public function populateFromTitles( $titles ) {
$this->profileIn();
- $this->initFromTitles($titles);
+ $this->initFromTitles( $titles );
$this->profileOut();
}
@@ -309,9 +309,9 @@ class ApiPageSet extends ApiQueryBase {
* Populate this PageSet from a list of page IDs
* @param $pageIDs array of page IDs
*/
- public function populateFromPageIDs($pageIDs) {
+ public function populateFromPageIDs( $pageIDs ) {
$this->profileIn();
- $this->initFromPageIds($pageIDs);
+ $this->initFromPageIds( $pageIDs );
$this->profileOut();
}
@@ -320,9 +320,9 @@ class ApiPageSet extends ApiQueryBase {
* @param $db Database object
* @param $queryResult Query result object
*/
- public function populateFromQueryResult($db, $queryResult) {
+ public function populateFromQueryResult( $db, $queryResult ) {
$this->profileIn();
- $this->initFromQueryResult($db, $queryResult);
+ $this->initFromQueryResult( $db, $queryResult );
$this->profileOut();
}
@@ -330,9 +330,9 @@ class ApiPageSet extends ApiQueryBase {
* Populate this PageSet from a list of revision IDs
* @param $revIDs array of revision IDs
*/
- public function populateFromRevisionIDs($revIDs) {
+ public function populateFromRevisionIDs( $revIDs ) {
$this->profileIn();
- $this->initFromRevIDs($revIDs);
+ $this->initFromRevIDs( $revIDs );
$this->profileOut();
}
@@ -340,22 +340,22 @@ class ApiPageSet extends ApiQueryBase {
* Extract all requested fields from the row received from the database
* @param $row Result row
*/
- public function processDbRow($row) {
+ public function processDbRow( $row ) {
// Store Title object in various data structures
- $title = Title :: makeTitle($row->page_namespace, $row->page_title);
+ $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;
- if ($this->mResolveRedirects && $row->page_is_redirect == '1') {
+ if ( $this->mResolveRedirects && $row->page_is_redirect == '1' ) {
$this->mPendingRedirectIDs[$pageId] = $title;
} else {
$this->mGoodTitles[$pageId] = $title;
}
- foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues)
+ foreach ( $this->mRequestedPageFields as $fieldName => & $fieldValues )
$fieldValues[$pageId] = $row-> $fieldName;
}
@@ -384,24 +384,24 @@ class ApiPageSet extends ApiQueryBase {
*
* @param $titles array of Title objects or strings
*/
- private function initFromTitles($titles) {
+ private function initFromTitles( $titles ) {
// Get validated and normalized title objects
- $linkBatch = $this->processTitlesArray($titles);
- if($linkBatch->isEmpty())
+ $linkBatch = $this->processTitlesArray( $titles );
+ if ( $linkBatch->isEmpty() )
return;
$db = $this->getDB();
- $set = $linkBatch->constructSet('page', $db);
+ $set = $linkBatch->constructSet( 'page', $db );
// Get pageIDs data from the `page` table
$this->profileDBIn();
- $res = $db->select('page', $this->getPageTableFields(), $set,
- __METHOD__);
+ $res = $db->select( 'page', $this->getPageTableFields(), $set,
+ __METHOD__ );
$this->profileDBOut();
// Hack: get the ns:titles stored in array(ns => array(titles)) format
- $this->initFromQueryResult($db, $res, $linkBatch->data, true); // process Titles
+ $this->initFromQueryResult( $db, $res, $linkBatch->data, true ); // process Titles
// Resolve any found redirects
$this->resolvePendingRedirects();
@@ -411,11 +411,11 @@ class ApiPageSet extends ApiQueryBase {
* Does the same as initFromTitles(), but is based on page IDs instead
* @param $pageids array of page IDs
*/
- private function initFromPageIds($pageids) {
- if(!count($pageids))
+ private function initFromPageIds( $pageids ) {
+ if ( !count( $pageids ) )
return;
- $pageids = array_map('intval', $pageids); // paranoia
+ $pageids = array_map( 'intval', $pageids ); // paranoia
$set = array (
'page_id' => $pageids
);
@@ -423,12 +423,12 @@ class ApiPageSet extends ApiQueryBase {
// Get pageIDs data from the `page` table
$this->profileDBIn();
- $res = $db->select('page', $this->getPageTableFields(), $set,
- __METHOD__);
+ $res = $db->select( 'page', $this->getPageTableFields(), $set,
+ __METHOD__ );
$this->profileDBOut();
- $remaining = array_flip($pageids);
- $this->initFromQueryResult($db, $res, $remaining, false); // process PageIDs
+ $remaining = array_flip( $pageids );
+ $this->initFromQueryResult( $db, $res, $remaining, false ); // process PageIDs
// Resolve any found redirects
$this->resolvePendingRedirects();
@@ -445,34 +445,34 @@ class ApiPageSet extends ApiQueryBase {
* If true, treat $remaining as an array of [ns][title]
* If false, treat it as an array of [pageIDs]
*/
- 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');
+ 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)) {
+ while ( $row = $db->fetchObject( $res ) ) {
- $pageId = intval($row->page_id);
+ $pageId = intval( $row->page_id );
// Remove found page from the list of remaining items
- if (isset($remaining)) {
- if ($processTitles)
- unset ($remaining[$row->page_namespace][$row->page_title]);
+ if ( isset( $remaining ) ) {
+ if ( $processTitles )
+ unset ( $remaining[$row->page_namespace][$row->page_title] );
else
- unset ($remaining[$pageId]);
+ unset ( $remaining[$pageId] );
}
// Store any extra fields requested by modules
- $this->processDbRow($row);
+ $this->processDbRow( $row );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if(isset($remaining)) {
+ if ( isset( $remaining ) ) {
// Any items left in the $remaining list are added as missing
- if($processTitles) {
+ if ( $processTitles ) {
// The remaining titles in $remaining are non-existent pages
- foreach ($remaining as $ns => $dbkeys) {
+ foreach ( $remaining as $ns => $dbkeys ) {
foreach ( $dbkeys as $dbkey => $unused ) {
- $title = Title :: makeTitle($ns, $dbkey);
+ $title = Title :: makeTitle( $ns, $dbkey );
$this->mAllPages[$ns][$dbkey] = $this->mFakePageId;
$this->mMissingTitles[$this->mFakePageId] = $title;
$this->mFakePageId--;
@@ -483,10 +483,10 @@ class ApiPageSet extends ApiQueryBase {
else
{
// The remaining pageids do not exist
- if(!$this->mMissingPageIDs)
- $this->mMissingPageIDs = array_keys($remaining);
+ if ( !$this->mMissingPageIDs )
+ $this->mMissingPageIDs = array_keys( $remaining );
else
- $this->mMissingPageIDs = array_merge($this->mMissingPageIDs, array_keys($remaining));
+ $this->mMissingPageIDs = array_merge( $this->mMissingPageIDs, array_keys( $remaining ) );
}
}
}
@@ -496,37 +496,37 @@ class ApiPageSet extends ApiQueryBase {
* instead
* @param $revids array of revision IDs
*/
- private function initFromRevIDs($revids) {
+ private function initFromRevIDs( $revids ) {
- if(!count($revids))
+ if ( !count( $revids ) )
return;
- $revids = array_map('intval', $revids); // paranoia
+ $revids = array_map( 'intval', $revids ); // paranoia
$db = $this->getDB();
$pageids = array();
- $remaining = array_flip($revids);
+ $remaining = array_flip( $revids );
- $tables = array('revision', 'page');
- $fields = array('rev_id', 'rev_page');
- $where = array('rev_id' => $revids, 'rev_page = page_id');
+ $tables = array( 'revision', 'page' );
+ $fields = array( 'rev_id', 'rev_page' );
+ $where = array( 'rev_id' => $revids, 'rev_page = page_id' );
// Get pageIDs data from the `page` table
$this->profileDBIn();
- $res = $db->select($tables, $fields, $where, __METHOD__);
- while ($row = $db->fetchObject($res)) {
- $revid = intval($row->rev_id);
- $pageid = intval($row->rev_page);
+ $res = $db->select( $tables, $fields, $where, __METHOD__ );
+ while ( $row = $db->fetchObject( $res ) ) {
+ $revid = intval( $row->rev_id );
+ $pageid = intval( $row->rev_page );
$this->mGoodRevIDs[$revid] = $pageid;
$pageids[$pageid] = '';
- unset($remaining[$revid]);
+ unset( $remaining[$revid] );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
$this->profileDBOut();
- $this->mMissingRevIDs = array_keys($remaining);
+ $this->mMissingRevIDs = array_keys( $remaining );
// Populate all the page information
- $this->initFromPageIds(array_keys($pageids));
+ $this->initFromPageIds( array_keys( $pageids ) );
}
/**
@@ -536,32 +536,32 @@ class ApiPageSet extends ApiQueryBase {
*/
private function resolvePendingRedirects() {
- if($this->mResolveRedirects) {
+ 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 ($this->mPendingRedirectIDs) {
+ while ( $this->mPendingRedirectIDs ) {
// Resolve redirects by querying the pagelinks table, and repeat the process
// Create a new linkBatch object for the next pass
$linkBatch = $this->getRedirectTargets();
- if ($linkBatch->isEmpty())
+ if ( $linkBatch->isEmpty() )
break;
- $set = $linkBatch->constructSet('page', $db);
- if($set === false)
+ $set = $linkBatch->constructSet( 'page', $db );
+ if ( $set === false )
break;
// Get pageIDs data from the `page` table
$this->profileDBIn();
- $res = $db->select('page', $pageFlds, $set, __METHOD__);
+ $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);
+ $this->initFromQueryResult( $db, $res, $linkBatch->data, true );
}
}
}
@@ -578,40 +578,40 @@ class ApiPageSet extends ApiQueryBase {
$db = $this->getDB();
$this->profileDBIn();
- $res = $db->select('redirect', array(
+ $res = $db->select( 'redirect', array(
'rd_from',
'rd_namespace',
'rd_title'
- ), array('rd_from' => array_keys($this->mPendingRedirectIDs)),
+ ), array( 'rd_from' => array_keys( $this->mPendingRedirectIDs ) ),
__METHOD__
);
$this->profileDBOut();
- while($row = $db->fetchObject($res))
+ while ( $row = $db->fetchObject( $res ) )
{
- $rdfrom = intval($row->rd_from);
+ $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);
+ $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($this->mPendingRedirectIDs)
+ $db->freeResult( $res );
+ if ( $this->mPendingRedirectIDs )
{
- # We found pages that aren't in the redirect table
- # Add them
- foreach($this->mPendingRedirectIDs as $id => $title)
+ // We found pages that aren't in the redirect table
+ // Add them
+ foreach ( $this->mPendingRedirectIDs as $id => $title )
{
- $article = new Article($title);
+ $article = new Article( $title );
$rt = $article->insertRedirect();
- if(!$rt)
- # What the hell. Let's just ignore this
+ if ( !$rt )
+ // What the hell. Let's just ignore this
continue;
- $lb->addObj($rt);
+ $lb->addObj( $rt );
$this->mRedirectTitles[$title->getPrefixedText()] = $rt->getPrefixedText();
- unset($this->mPendingRedirectIDs[$id]);
+ unset( $this->mPendingRedirectIDs[$id] );
}
}
return $lb;
@@ -626,32 +626,32 @@ class ApiPageSet extends ApiQueryBase {
* @param $titles array of Title objects or strings
* @return LinkBatch
*/
- private function processTitlesArray($titles) {
+ private function processTitlesArray( $titles ) {
$linkBatch = new LinkBatch();
- foreach ($titles as $title) {
+ foreach ( $titles as $title ) {
- $titleObj = is_string($title) ? Title :: newFromText($title) : $title;
- if (!$titleObj)
+ $titleObj = is_string( $title ) ? Title :: newFromText( $title ) : $title;
+ if ( !$titleObj )
{
- # Handle invalid titles gracefully
+ // 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 (strval($iw) !== '') {
+ if ( strval( $iw ) !== '' ) {
// This title is an interwiki link.
$this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw;
} else {
// Validation
- if ($titleObj->getNamespace() < 0)
- $this->setWarning("No support for special pages has been implemented");
+ if ( $titleObj->getNamespace() < 0 )
+ $this->setWarning( "No support for special pages has been implemented" );
else
- $linkBatch->addObj($titleObj);
+ $linkBatch->addObj( $titleObj );
}
// Make sure we remember the original title that was
@@ -659,7 +659,7 @@ class ApiPageSet extends ApiQueryBase {
// titles with the originally requested when e.g. the
// namespace is localized or the capitalization is
// different
- if (is_string($title) && $title !== $titleObj->getPrefixedText()) {
+ if ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) {
$this->mNormalizedTitles[$title] = $titleObj->getPrefixedText();
}
}
@@ -691,7 +691,14 @@ class ApiPageSet extends ApiQueryBase {
);
}
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'multisource', 'info' => "Cannot use 'pageids' at the same time as 'dataSource'" ),
+ array( 'code' => 'multisource', 'info' => "Cannot use 'revids' at the same time as 'dataSource'" ),
+ ) );
+ }
+
public function getVersion() {
- return __CLASS__ . ': $Id: ApiPageSet.php 47424 2009-02-18 05:29:11Z werdna $';
+ return __CLASS__ . ': $Id: ApiPageSet.php 62410 2010-02-13 01:21:52Z reedy $';
}
}
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index d710c206..8fe2cad2 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -33,123 +33,149 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiParamInfo extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
// Get parameters
$params = $this->extractRequestParams();
$result = $this->getResult();
- $queryObj = new ApiQuery($this->getMain(), 'query');
+ $queryObj = new ApiQuery( $this->getMain(), 'query' );
$r = array();
- if(is_array($params['modules']))
+ if ( is_array( $params['modules'] ) )
{
$modArr = $this->getMain()->getModules();
- foreach($params['modules'] as $m)
+ $r['modules'] = array();
+ foreach ( $params['modules'] as $m )
{
- if(!isset($modArr[$m]))
+ if ( !isset( $modArr[$m] ) )
{
- $r['modules'][] = array('name' => $m, 'missing' => '');
+ $r['modules'][] = array( 'name' => $m, 'missing' => '' );
continue;
}
- $obj = new $modArr[$m]($this->getMain(), $m);
- $a = $this->getClassInfo($obj);
+ $obj = new $modArr[$m]( $this->getMain(), $m );
+ $a = $this->getClassInfo( $obj );
$a['name'] = $m;
$r['modules'][] = $a;
}
- $result->setIndexedTagName($r['modules'], 'module');
+ $result->setIndexedTagName( $r['modules'], 'module' );
}
- if(is_array($params['querymodules']))
+ if ( is_array( $params['querymodules'] ) )
{
$qmodArr = $queryObj->getModules();
- foreach($params['querymodules'] as $qm)
+ $r['querymodules'] = array();
+ foreach ( $params['querymodules'] as $qm )
{
- if(!isset($qmodArr[$qm]))
+ if ( !isset( $qmodArr[$qm] ) )
{
- $r['querymodules'][] = array('name' => $qm, 'missing' => '');
+ $r['querymodules'][] = array( 'name' => $qm, 'missing' => '' );
continue;
}
- $obj = new $qmodArr[$qm]($this, $qm);
- $a = $this->getClassInfo($obj);
+ $obj = new $qmodArr[$qm]( $this, $qm );
+ $a = $this->getClassInfo( $obj );
$a['name'] = $qm;
$r['querymodules'][] = $a;
}
- $result->setIndexedTagName($r['querymodules'], 'module');
+ $result->setIndexedTagName( $r['querymodules'], 'module' );
}
- if($params['mainmodule'])
- $r['mainmodule'] = $this->getClassInfo($this->getMain());
- if($params['pagesetmodule'])
+ if ( $params['mainmodule'] )
+ $r['mainmodule'] = $this->getClassInfo( $this->getMain() );
+ if ( $params['pagesetmodule'] )
{
- $pageSet = new ApiPageSet($queryObj);
- $r['pagesetmodule'] = $this->getClassInfo($pageSet);
+ $pageSet = new ApiPageSet( $queryObj );
+ $r['pagesetmodule'] = $this->getClassInfo( $pageSet );
}
- $result->addValue(null, $this->getModuleName(), $r);
+ $result->addValue( null, $this->getModuleName(), $r );
}
- function getClassInfo($obj)
+ function getClassInfo( $obj )
{
$result = $this->getResult();
- $retval['classname'] = get_class($obj);
- $retval['description'] = (is_array($obj->getDescription()) ? implode("\n", $obj->getDescription()) : $obj->getDescription());
+ $retval['classname'] = get_class( $obj );
+ $retval['description'] = implode( "\n", (array)$obj->getDescription() );
+ $retval['version'] = implode( "\n", (array)$obj->getVersion() );
$retval['prefix'] = $obj->getModulePrefix();
- if($obj->isReadMode())
+
+ if ( $obj->isReadMode() )
$retval['readrights'] = '';
- if($obj->isWriteMode())
+ if ( $obj->isWriteMode() )
$retval['writerights'] = '';
- if($obj->mustBePosted())
+ if ( $obj->mustBePosted() )
$retval['mustbeposted'] = '';
+ if ( $obj instanceof ApiQueryGeneratorBase )
+ $retval['generator'] = '';
+
$allowedParams = $obj->getFinalParams();
- if(!is_array($allowedParams))
+ if ( !is_array( $allowedParams ) )
return $retval;
+
$retval['parameters'] = array();
$paramDesc = $obj->getFinalParamDescription();
- foreach($allowedParams as $n => $p)
+ foreach ( $allowedParams as $n => $p )
{
- $a = array('name' => $n);
- if(!is_array($p))
+ $a = array( 'name' => $n );
+ if ( isset( $paramDesc[$n] ) )
+ $a['description'] = implode( "\n", (array)$paramDesc[$n] );
+ if ( isset( $p[ApiBase::PARAM_DEPRECATED] ) && $p[ApiBase::PARAM_DEPRECATED] )
+ $a['deprecated'] = '';
+ if ( !is_array( $p ) )
{
- if(is_bool($p))
+ if ( is_bool( $p ) )
{
$a['type'] = 'bool';
- $a['default'] = ($p ? 'true' : 'false');
+ $a['default'] = ( $p ? 'true' : 'false' );
+ }
+ else if ( is_string( $p ) || is_null( $p ) )
+ {
+ $a['type'] = 'string';
+ $a['default'] = strval( $p );
+ }
+ else if ( is_int( $p ) )
+ {
+ $a['type'] = 'integer';
+ $a['default'] = intval( $p );
}
- if(is_string($p))
- $a['default'] = $p;
$retval['parameters'][] = $a;
continue;
}
- if(isset($p[ApiBase::PARAM_DFLT]))
+ if ( isset( $p[ApiBase::PARAM_DFLT] ) )
$a['default'] = $p[ApiBase::PARAM_DFLT];
- if(isset($p[ApiBase::PARAM_ISMULTI]))
- if($p[ApiBase::PARAM_ISMULTI])
+ if ( isset( $p[ApiBase::PARAM_ISMULTI] ) )
+ if ( $p[ApiBase::PARAM_ISMULTI] )
{
$a['multi'] = '';
$a['limit'] = $this->getMain()->canApiHighLimits() ?
ApiBase::LIMIT_SML2 :
ApiBase::LIMIT_SML1;
}
- if(isset($p[ApiBase::PARAM_ALLOW_DUPLICATES]))
- if($p[ApiBase::PARAM_ALLOW_DUPLICATES])
+
+ if ( isset( $p[ApiBase::PARAM_ALLOW_DUPLICATES] ) )
+ if ( $p[ApiBase::PARAM_ALLOW_DUPLICATES] )
$a['allowsduplicates'] = '';
- if(isset($p[ApiBase::PARAM_TYPE]))
+
+ if ( isset( $p[ApiBase::PARAM_TYPE] ) )
{
$a['type'] = $p[ApiBase::PARAM_TYPE];
- if(is_array($a['type']))
- $result->setIndexedTagName($a['type'], 't');
+ if ( is_array( $a['type'] ) )
+ $result->setIndexedTagName( $a['type'], 't' );
}
- if(isset($p[ApiBase::PARAM_MAX]))
+ if ( isset( $p[ApiBase::PARAM_MAX] ) )
$a['max'] = $p[ApiBase::PARAM_MAX];
- if(isset($p[ApiBase::PARAM_MAX2]))
+ if ( isset( $p[ApiBase::PARAM_MAX2] ) )
$a['highmax'] = $p[ApiBase::PARAM_MAX2];
- if(isset($p[ApiBase::PARAM_MIN]))
+ if ( isset( $p[ApiBase::PARAM_MIN] ) )
$a['min'] = $p[ApiBase::PARAM_MIN];
- if(isset($paramDesc[$n]))
- $a['description'] = (is_array($paramDesc[$n]) ? implode("\n", $paramDesc[$n]) : $paramDesc[$n]);
$retval['parameters'][] = $a;
}
- $result->setIndexedTagName($retval['parameters'], 'param');
+ $result->setIndexedTagName( $retval['parameters'], 'param' );
+
+ // Errors
+ $retval['errors'] = $this->parseErrors( $obj->getPossibleErrors() );
+
+ $result->setIndexedTagName( $retval['errors'], 'error' );
+
return $retval;
}
@@ -190,6 +216,6 @@ class ApiParamInfo extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiParamInfo.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiParamInfo.php 62336 2010-02-11 22:22:20Z reedy $';
}
}
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index c8cd07fe..db389bdb 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -33,8 +33,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiParse extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
@@ -47,119 +47,140 @@ class ApiParse extends ApiBase {
$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']);
+ 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;
// The parser needs $wgTitle to be set, apparently the
// $title parameter in Parser::parse isn't enough *sigh*
- global $wgParser, $wgUser, $wgTitle;
+ global $wgParser, $wgUser, $wgTitle, $wgEnableParserCache;
$popts = new ParserOptions();
- $popts->setTidy(true);
+ $popts->setTidy( true );
$popts->enableLimitReport();
$redirValues = null;
- if(!is_null($oldid) || !is_null($page))
+ if ( !is_null( $oldid ) || !is_null( $page ) )
{
- if(!is_null($oldid))
+ 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');
+ // 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->getText( Revision::FOR_THIS_USER );
$titleObj = $rev->getTitle();
$wgTitle = $titleObj;
- $p_result = $wgParser->parse($text, $titleObj, $popts);
+ $p_result = $wgParser->parse( $text, $titleObj, $popts );
}
else
{
- if($params['redirects'])
+ if ( $params['redirects'] )
{
- $req = new FauxRequest(array(
+ $req = new FauxRequest( array(
'action' => 'query',
'redirects' => '',
'titles' => $page
- ));
- $main = new ApiMain($req);
+ ) );
+ $main = new ApiMain( $req );
$main->execute();
$data = $main->getResultData();
$redirValues = @$data['query']['redirects'];
$to = $page;
- foreach((array)$redirValues as $r)
+ foreach ( (array)$redirValues as $r )
$to = $r['to'];
}
else
- $to = $page;
- $titleObj = Title::newFromText($to);
- if(!$titleObj)
- $this->dieUsage("The page you specified doesn't exist", 'missingtitle');
+ $to = $page;
+ $titleObj = Title::newFromText( $to );
+ if ( !$titleObj )
+ $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' );
- $articleObj = new Article($titleObj);
- if(isset($prop['revid']))
+ $articleObj = new Article( $titleObj );
+ if ( isset( $prop['revid'] ) )
$oldid = $articleObj->getRevIdFetched();
// Try the parser cache first
+ $p_result = false;
$pcache = ParserCache::singleton();
- $p_result = $pcache->get($articleObj, $wgUser);
- if(!$p_result)
+ if ( $wgEnableParserCache )
+ $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, $popts);
+ $p_result = $wgParser->parse( $articleObj->getContent(), $titleObj, $popts );
+
+ if ( $wgEnableParserCache )
+ $pcache->save( $p_result, $articleObj, $popts );
}
}
}
else
{
- $titleObj = Title::newFromText($title);
- if(!$titleObj)
- $titleObj = Title::newFromText("API");
+ $titleObj = Title::newFromText( $title );
+ if ( !$titleObj )
+ $titleObj = Title::newFromText( "API" );
$wgTitle = $titleObj;
- if($params['pst'] || $params['onlypst'])
- $text = $wgParser->preSaveTransform($text, $titleObj, $wgUser, $popts);
- if($params['onlypst'])
+ if ( $params['pst'] || $params['onlypst'] )
+ $text = $wgParser->preSaveTransform( $text, $titleObj, $wgUser, $popts );
+ if ( $params['onlypst'] )
{
// Build a result and bail out
$result_array['text'] = array();
- $this->getResult()->setContent($result_array['text'], $text);
- $this->getResult()->addValue(null, $this->getModuleName(), $result_array);
+ $this->getResult()->setContent( $result_array['text'], $text );
+ $this->getResult()->addValue( null, $this->getModuleName(), $result_array );
return;
}
- $p_result = $wgParser->parse($text, $titleObj, $popts);
+ $p_result = $wgParser->parse( $text, $titleObj, $popts );
}
// Return result
$result = $this->getResult();
$result_array = array();
- if($params['redirects'] && !is_null($redirValues))
+ if ( $params['redirects'] && !is_null( $redirValues ) )
$result_array['redirects'] = $redirValues;
- if(isset($prop['text'])) {
+
+ if ( isset( $prop['text'] ) ) {
$result_array['text'] = array();
- $result->setContent($result_array['text'], $p_result->getText());
+ $result->setContent( $result_array['text'], $p_result->getText() );
+ }
+
+ if ( !is_null( $params['summary'] ) ) {
+ $result_array['parsedsummary'] = array();
+ $result->setContent( $result_array['parsedsummary'], $wgUser->getSkin()->formatComment( $params['summary'], $titleObj ) );
}
- if(isset($prop['langlinks']))
- $result_array['langlinks'] = $this->formatLangLinks($p_result->getLanguageLinks());
- if(isset($prop['categories']))
- $result_array['categories'] = $this->formatCategoryLinks($p_result->getCategories());
- if(isset($prop['links']))
- $result_array['links'] = $this->formatLinks($p_result->getLinks());
- if(isset($prop['templates']))
- $result_array['templates'] = $this->formatLinks($p_result->getTemplates());
- if(isset($prop['images']))
- $result_array['images'] = array_keys($p_result->getImages());
- if(isset($prop['externallinks']))
- $result_array['externallinks'] = array_keys($p_result->getExternalLinks());
- if(isset($prop['sections']))
+
+ if ( isset( $prop['langlinks'] ) )
+ $result_array['langlinks'] = $this->formatLangLinks( $p_result->getLanguageLinks() );
+ if ( isset( $prop['categories'] ) )
+ $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() );
+ if ( isset( $prop['links'] ) )
+ $result_array['links'] = $this->formatLinks( $p_result->getLinks() );
+ if ( isset( $prop['templates'] ) )
+ $result_array['templates'] = $this->formatLinks( $p_result->getTemplates() );
+ if ( isset( $prop['images'] ) )
+ $result_array['images'] = array_keys( $p_result->getImages() );
+ if ( isset( $prop['externallinks'] ) )
+ $result_array['externallinks'] = array_keys( $p_result->getExternalLinks() );
+ if ( isset( $prop['sections'] ) )
$result_array['sections'] = $p_result->getSections();
- if(isset($prop['displaytitle']))
+ if ( isset( $prop['displaytitle'] ) )
$result_array['displaytitle'] = $p_result->getDisplayTitle() ?
$p_result->getDisplayTitle() :
$titleObj->getPrefixedText();
- if(!is_null($oldid))
- $result_array['revid'] = intval($oldid);
+
+ if ( isset( $prop['headitems'] ) )
+ $result_array['headitems'] = $this->formatHeadItems( $p_result->getHeadItems() );
+
+ if ( isset( $prop['headhtml'] ) ) {
+ $out = new OutputPage;
+ $out->addParserOutputNoText( $p_result );
+ $result_array['headhtml'] = array();
+ $result->setContent( $result_array['headhtml'], $out->headElement( $wgUser->getSkin() ) );
+ }
+
+ if ( !is_null( $oldid ) )
+ $result_array['revid'] = intval( $oldid );
$result_mapping = array(
'redirects' => 'r',
@@ -170,6 +191,7 @@ class ApiParse extends ApiBase {
'images' => 'img',
'externallinks' => 'el',
'sections' => 's',
+ 'headitems' => 'hi'
);
$this->setIndexedTagNames( $result_array, $result_mapping );
$result->addValue( null, $this->getModuleName(), $result_array );
@@ -177,9 +199,9 @@ class ApiParse extends ApiBase {
private function formatLangLinks( $links ) {
$result = array();
- foreach( $links as $link ) {
+ foreach ( $links as $link ) {
$entry = array();
- $bits = split( ':', $link, 2 );
+ $bits = explode( ':', $link, 2 );
$entry['lang'] = $bits[0];
$this->getResult()->setContent( $entry, $bits[1] );
$result[] = $entry;
@@ -189,7 +211,7 @@ class ApiParse extends ApiBase {
private function formatCategoryLinks( $links ) {
$result = array();
- foreach( $links as $link => $sortkey ) {
+ foreach ( $links as $link => $sortkey ) {
$entry = array();
$entry['sortkey'] = $sortkey;
$this->getResult()->setContent( $entry, $link );
@@ -200,12 +222,12 @@ class ApiParse extends ApiBase {
private function formatLinks( $links ) {
$result = array();
- foreach( $links as $ns => $nslinks ) {
- foreach( $nslinks as $title => $id ) {
+ foreach ( $links as $ns => $nslinks ) {
+ foreach ( $nslinks as $title => $id ) {
$entry = array();
$entry['ns'] = $ns;
$this->getResult()->setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() );
- if( $id != 0 )
+ if ( $id != 0 )
$entry['exists'] = '';
$result[] = $entry;
}
@@ -213,9 +235,20 @@ class ApiParse extends ApiBase {
return $result;
}
+ private function formatHeadItems( $headItems ) {
+ $result = array();
+ foreach ( $headItems as $tag => $content ) {
+ $entry = array();
+ $entry['tag'] = $tag;
+ $this->getResult()->setContent( $entry, $content );
+ $result[] = $entry;
+ }
+ return $result;
+ }
+
private function setIndexedTagNames( &$array, $mapping ) {
- foreach( $mapping as $key => $name ) {
- if( isset( $array[$key] ) )
+ foreach ( $mapping as $key => $name ) {
+ if ( isset( $array[$key] ) )
$this->getResult()->setIndexedTagName( $array[$key], $name );
}
}
@@ -226,6 +259,7 @@ class ApiParse extends ApiBase {
ApiBase :: PARAM_DFLT => 'API',
),
'text' => null,
+ 'summary' => null,
'page' => null,
'redirects' => false,
'oldid' => null,
@@ -243,6 +277,8 @@ class ApiParse extends ApiBase {
'sections',
'revid',
'displaytitle',
+ 'headitems',
+ 'headhtml'
)
),
'pst' => false,
@@ -253,17 +289,18 @@ class ApiParse extends ApiBase {
public function getParamDescription() {
return array (
'text' => 'Wikitext to parse',
+ 'summary' => 'Summary to parse',
'redirects' => 'If the page parameter is set to a redirect, resolve it',
'title' => 'Title of page the text belongs to',
'page' => 'Parse the content of this page. Cannot be used together with text and title',
'oldid' => 'Parse the content of this revision. Overrides page',
- 'prop' => array('Which pieces of information to get.',
+ 'prop' => array( 'Which pieces of information to get.',
'NOTE: Section tree is only generated if there are more than 4 sections, or if the __TOC__ keyword is present'
),
'pst' => array( 'Do a pre-save transform on the input before parsing it.',
'Ignored if page or oldid is used.'
),
- 'onlypst' => array('Do a PST on the input, but don\'t parse it.',
+ 'onlypst' => array( 'Do a PST on the input, but don\'t parse it.',
'Returns PSTed wikitext. Ignored if page or oldid is used.'
),
);
@@ -272,6 +309,15 @@ class ApiParse extends ApiBase {
public function getDescription() {
return 'This module parses wikitext and returns parser output';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'params', 'info' => 'The page parameter cannot be used together with the text and title parameters' ),
+ array( 'code' => 'missingrev', 'info' => 'There is no revision ID oldid' ),
+ array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revisions' ),
+ array( 'code' => 'missingtitle', 'info' => 'The page you specified doesn\'t exist' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -280,6 +326,6 @@ class ApiParse extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiParse.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiParse.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php
index a6f25af2..3b2b2046 100644
--- a/includes/api/ApiPatrol.php
+++ b/includes/api/ApiPatrol.php
@@ -23,8 +23,8 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
- require_once ('ApiBase.php');
+if ( !defined( 'MEDIAWIKI' ) ) {
+ require_once ( 'ApiBase.php' );
}
/**
@@ -33,35 +33,30 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiPatrol extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
/**
* Patrols the article or provides the reason the patrol failed.
*/
public function execute() {
- global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol;
$params = $this->extractRequestParams();
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
- if(!isset($params['rcid']))
- $this->dieUsageMsg(array('missingparam', 'rcid'));
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
+ if ( !isset( $params['rcid'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'rcid' ) );
- $rc = RecentChange::newFromID($params['rcid']);
- if(!$rc instanceof RecentChange)
- $this->dieUsageMsg(array('nosuchrcid', $params['rcid']));
- $retval = RecentChange::markPatrolled($params['rcid']);
+ $rc = RecentChange::newFromID( $params['rcid'] );
+ if ( !$rc instanceof RecentChange )
+ $this->dieUsageMsg( array( 'nosuchrcid', $params['rcid'] ) );
+ $retval = RecentChange::markPatrolled( $params['rcid'] );
- if($retval)
- $this->dieUsageMsg(reset($retval));
+ if ( $retval )
+ $this->dieUsageMsg( reset( $retval ) );
- $result = array('rcid' => intval($rc->getAttribute('rc_id')));
- ApiQueryBase::addTitleInfo($result, $rc->getTitle());
- $this->getResult()->addValue(null, $this->getModuleName(), $result);
+ $result = array( 'rcid' => intval( $rc->getAttribute( 'rc_id' ) ) );
+ ApiQueryBase::addTitleInfo( $result, $rc->getTitle() );
+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
}
public function isWriteMode() {
@@ -89,6 +84,17 @@ class ApiPatrol extends ApiBase {
'Patrol a page or revision. '
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'rcid' ),
+ array( 'nosuchrcid', 'rcid' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array(
@@ -97,6 +103,6 @@ class ApiPatrol extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiPatrol.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiPatrol.php 69578 2010-07-20 02:46:20Z tstarling $';
}
-}
+} \ No newline at end of file
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index aad37066..ca47c1b8 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -32,99 +32,100 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiProtect extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
global $wgUser, $wgRestrictionTypes, $wgRestrictionLevels;
$params = $this->extractRequestParams();
- $titleObj = NULL;
- if(!isset($params['title']))
- $this->dieUsageMsg(array('missingparam', 'title'));
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
- if(empty($params['protections']))
- $this->dieUsageMsg(array('missingparam', 'protections'));
+ $titleObj = null;
+ if ( !isset( $params['title'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'title' ) );
+ if ( empty( $params['protections'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'protections' ) );
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
+ $titleObj = Title::newFromText( $params['title'] );
+ if ( !$titleObj )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
- $titleObj = Title::newFromText($params['title']);
- if(!$titleObj)
- $this->dieUsageMsg(array('invalidtitle', $params['title']));
-
- $errors = $titleObj->getUserPermissionsErrors('protect', $wgUser);
- if($errors)
+ $errors = $titleObj->getUserPermissionsErrors( 'protect', $wgUser );
+ if ( $errors )
// We don't care about multiple errors, just report one of them
- $this->dieUsageMsg(reset($errors));
+ $this->dieUsageMsg( reset( $errors ) );
$expiry = (array)$params['expiry'];
- if(count($expiry) != count($params['protections']))
+ if ( count( $expiry ) != count( $params['protections'] ) )
{
- if(count($expiry) == 1)
- $expiry = array_fill(0, count($params['protections']), $expiry[0]);
+ if ( count( $expiry ) == 1 )
+ $expiry = array_fill( 0, count( $params['protections'] ), $expiry[0] );
else
- $this->dieUsageMsg(array('toofewexpiries', count($expiry), count($params['protections'])));
+ $this->dieUsageMsg( array( 'toofewexpiries', count( $expiry ), count( $params['protections'] ) ) );
}
+
+ $restrictionTypes = $titleObj->getRestrictionTypes();
$protections = array();
$expiryarray = array();
$resultProtections = array();
- foreach($params['protections'] as $i => $prot)
+ foreach ( $params['protections'] as $i => $prot )
{
- $p = explode('=', $prot);
- $protections[$p[0]] = ($p[1] == 'all' ? '' : $p[1]);
- if($titleObj->exists() && $p[0] == 'create')
- $this->dieUsageMsg(array('create-titleexists'));
- if(!$titleObj->exists() && $p[0] != 'create')
- $this->dieUsageMsg(array('missingtitles-createonly'));
- if(!in_array($p[0], $wgRestrictionTypes) && $p[0] != 'create')
- $this->dieUsageMsg(array('protect-invalidaction', $p[0]));
- if(!in_array($p[1], $wgRestrictionLevels) && $p[1] != 'all')
- $this->dieUsageMsg(array('protect-invalidlevel', $p[1]));
-
- if(in_array($expiry[$i], array('infinite', 'indefinite', 'never')))
+ $p = explode( '=', $prot );
+ $protections[$p[0]] = ( $p[1] == 'all' ? '' : $p[1] );
+
+ if ( $titleObj->exists() && $p[0] == 'create' )
+ $this->dieUsageMsg( array( 'create-titleexists' ) );
+ if ( !$titleObj->exists() && $p[0] != 'create' )
+ $this->dieUsageMsg( array( 'missingtitle-createonly' ) );
+
+ if ( !in_array( $p[0], $restrictionTypes ) && $p[0] != 'create' )
+ $this->dieUsageMsg( array( 'protect-invalidaction', $p[0] ) );
+ if ( !in_array( $p[1], $wgRestrictionLevels ) && $p[1] != 'all' )
+ $this->dieUsageMsg( array( 'protect-invalidlevel', $p[1] ) );
+
+ if ( in_array( $expiry[$i], array( 'infinite', 'indefinite', 'never' ) ) )
$expiryarray[$p[0]] = Block::infinity();
else
{
- $exp = strtotime($expiry[$i]);
- if($exp < 0 || $exp == false)
- $this->dieUsageMsg(array('invalidexpiry', $expiry[$i]));
+ $exp = strtotime( $expiry[$i] );
+ if ( $exp < 0 || $exp == false )
+ $this->dieUsageMsg( array( 'invalidexpiry', $expiry[$i] ) );
- $exp = wfTimestamp(TS_MW, $exp);
- if($exp < wfTimestampNow())
- $this->dieUsageMsg(array('pastexpiry', $expiry[$i]));
+ $exp = wfTimestamp( TS_MW, $exp );
+ if ( $exp < wfTimestampNow() )
+ $this->dieUsageMsg( array( 'pastexpiry', $expiry[$i] ) );
$expiryarray[$p[0]] = $exp;
}
- $resultProtections[] = array($p[0] => $protections[$p[0]],
- 'expiry' => ($expiryarray[$p[0]] == Block::infinity() ?
+ $resultProtections[] = array( $p[0] => $protections[$p[0]],
+ 'expiry' => ( $expiryarray[$p[0]] == Block::infinity() ?
'infinite' :
- wfTimestamp(TS_ISO_8601, $expiryarray[$p[0]])));
+ wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] ) ) );
}
$cascade = $params['cascade'];
- $articleObj = new Article($titleObj);
- if($params['watch'])
+ $articleObj = new Article( $titleObj );
+ if ( $params['watch'] )
$articleObj->doWatch();
- if($titleObj->exists())
- $ok = $articleObj->updateRestrictions($protections, $params['reason'], $cascade, $expiryarray);
+ if ( $titleObj->exists() )
+ $ok = $articleObj->updateRestrictions( $protections, $params['reason'], $cascade, $expiryarray );
else
- $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiryarray['create']);
- if(!$ok)
+ $ok = $titleObj->updateTitleProtection( $protections['create'], $params['reason'], $expiryarray['create'] );
+ if ( !$ok )
// This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime?
// Just throw an unknown error in this case, as it's very likely to be a race condition
- $this->dieUsageMsg(array());
- $res = array('title' => $titleObj->getPrefixedText(), 'reason' => $params['reason']);
- if($cascade)
+ $this->dieUsageMsg( array() );
+ $res = array( 'title' => $titleObj->getPrefixedText(), 'reason' => $params['reason'] );
+ if ( $cascade )
$res['cascade'] = '';
$res['protections'] = $resultProtections;
- $this->getResult()->setIndexedTagName($res['protections'], 'protection');
- $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ $this->getResult()->setIndexedTagName( $res['protections'], 'protection' );
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
@@ -153,11 +154,11 @@ class ApiProtect extends ApiBase {
'title' => 'Title of the page you want to (un)protect.',
'token' => 'A protect token previously retrieved through prop=info',
'protections' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)',
- 'expiry' => array('Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.',
- 'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.'),
+ 'expiry' => array( 'Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.',
+ 'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.' ),
'reason' => 'Reason for (un)protecting (optional)',
- 'cascade' => array('Enable cascading protection (i.e. protect pages included in this page)',
- 'Ignored if not all protection levels are \'sysop\' or \'protect\''),
+ 'cascade' => array( 'Enable cascading protection (i.e. protect pages included in this page)',
+ 'Ignored if not all protection levels are \'sysop\' or \'protect\'' ),
'watch' => 'If set, add the page being (un)protected to your watchlist',
);
}
@@ -167,6 +168,25 @@ class ApiProtect extends ApiBase {
'Change the protection level of a page.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'title' ),
+ array( 'missingparam', 'protections' ),
+ array( 'invalidtitle', 'title' ),
+ array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ),
+ array( 'create-titleexists' ),
+ array( 'missingtitle-createonly' ),
+ array( 'protect-invalidaction', 'action' ),
+ array( 'protect-invalidlevel', 'level' ),
+ array( 'invalidexpiry', 'expiry' ),
+ array( 'pastexpiry', 'expiry' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return null;
+ }
protected function getExamples() {
return array (
@@ -176,6 +196,6 @@ class ApiProtect extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiProtect.php 48122 2009-03-07 12:58:41Z catrope $';
+ return __CLASS__ . ': $Id: ApiProtect.php 62557 2010-02-15 23:53:43Z reedy $';
}
}
diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php
index d1e6824d..76d45404 100644
--- a/includes/api/ApiPurge.php
+++ b/includes/api/ApiPurge.php
@@ -23,8 +23,8 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
- require_once ('ApiBase.php');
+if ( !defined( 'MEDIAWIKI' ) ) {
+ require_once ( 'ApiBase.php' );
}
/**
@@ -33,8 +33,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiPurge extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
/**
@@ -43,35 +43,35 @@ class ApiPurge extends ApiBase {
public function execute() {
global $wgUser;
$params = $this->extractRequestParams();
- if(!$wgUser->isAllowed('purge'))
- $this->dieUsageMsg(array('cantpurge'));
- if(!isset($params['titles']))
- $this->dieUsageMsg(array('missingparam', 'titles'));
+ if ( !$wgUser->isAllowed( 'purge' ) )
+ $this->dieUsageMsg( array( 'cantpurge' ) );
+ if ( !isset( $params['titles'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'titles' ) );
$result = array();
- foreach($params['titles'] as $t) {
+ foreach ( $params['titles'] as $t ) {
$r = array();
- $title = Title::newFromText($t);
- if(!$title instanceof Title)
+ $title = Title::newFromText( $t );
+ if ( !$title instanceof Title )
{
$r['title'] = $t;
$r['invalid'] = '';
$result[] = $r;
continue;
}
- ApiQueryBase::addTitleInfo($r, $title);
- if(!$title->exists())
+ ApiQueryBase::addTitleInfo( $r, $title );
+ if ( !$title->exists() )
{
$r['missing'] = '';
$result[] = $r;
continue;
}
- $article = new Article($title);
+ $article = Mediawiki::articleFromTitle( $title );
$article->doPurge(); // Directly purge and skip the UI part of purge().
$r['purged'] = '';
$result[] = $r;
}
- $this->getResult()->setIndexedTagName($result, 'page');
- $this->getResult()->addValue(null, $this->getModuleName(), $result);
+ $this->getResult()->setIndexedTagName( $result, 'page' );
+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
}
public function mustBePosted() {
@@ -102,6 +102,13 @@ class ApiPurge extends ApiBase {
'Purge the cache for the given titles.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'cantpurge' ),
+ array( 'missingparam', 'titles' ),
+ ) );
+ }
protected function getExamples() {
return array(
@@ -110,6 +117,6 @@ class ApiPurge extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiPurge.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiPurge.php 69578 2010-07-20 02:46:20Z tstarling $';
}
}
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index 49ddcdd3..8d3ef616 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -74,6 +74,7 @@ class ApiQuery extends ApiBase {
'logevents' => 'ApiQueryLogEvents',
'recentchanges' => 'ApiQueryRecentChanges',
'search' => 'ApiQuerySearch',
+ 'tags' => 'ApiQueryTags',
'usercontribs' => 'ApiQueryContributions',
'watchlist' => 'ApiQueryWatchlist',
'watchlistraw' => 'ApiQueryWatchlistRaw',
@@ -92,22 +93,22 @@ class ApiQuery extends ApiBase {
private $mSlaveDB = null;
private $mNamedDB = array();
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
// Allow custom modules to be added in LocalSettings.php
global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules;
- self :: appendUserModules($this->mQueryPropModules, $wgAPIPropModules);
- self :: appendUserModules($this->mQueryListModules, $wgAPIListModules);
- self :: appendUserModules($this->mQueryMetaModules, $wgAPIMetaModules);
+ self :: appendUserModules( $this->mQueryPropModules, $wgAPIPropModules );
+ self :: appendUserModules( $this->mQueryListModules, $wgAPIListModules );
+ self :: appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules );
- $this->mPropModuleNames = array_keys($this->mQueryPropModules);
- $this->mListModuleNames = array_keys($this->mQueryListModules);
- $this->mMetaModuleNames = array_keys($this->mQueryMetaModules);
+ $this->mPropModuleNames = array_keys( $this->mQueryPropModules );
+ $this->mListModuleNames = array_keys( $this->mQueryListModules );
+ $this->mMetaModuleNames = array_keys( $this->mQueryMetaModules );
// Allow the entire list of modules at first,
// but during module instantiation check if it can be used as a generator.
- $this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames);
+ $this->mAllowedGenerators = array_merge( $this->mListModuleNames, $this->mPropModuleNames );
}
/**
@@ -115,9 +116,9 @@ class ApiQuery extends ApiBase {
* @param $modules array Module array
* @param $newModules array Module array to add to $modules
*/
- private static function appendUserModules(&$modules, $newModules) {
- if (is_array( $newModules )) {
- foreach ( $newModules as $moduleName => $moduleClass) {
+ private static function appendUserModules( &$modules, $newModules ) {
+ if ( is_array( $newModules ) ) {
+ foreach ( $newModules as $moduleName => $moduleClass ) {
$modules[$moduleName] = $moduleClass;
}
}
@@ -128,9 +129,9 @@ class ApiQuery extends ApiBase {
* @return Database
*/
public function getDB() {
- if (!isset ($this->mSlaveDB)) {
+ if ( !isset ( $this->mSlaveDB ) ) {
$this->profileDBIn();
- $this->mSlaveDB = wfGetDB(DB_SLAVE,'api');
+ $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' );
$this->profileDBOut();
}
return $this->mSlaveDB;
@@ -146,10 +147,10 @@ class ApiQuery extends ApiBase {
* @param $groups array Query groups
* @return Database
*/
- public function getNamedDB($name, $db, $groups) {
- if (!array_key_exists($name, $this->mNamedDB)) {
+ public function getNamedDB( $name, $db, $groups ) {
+ if ( !array_key_exists( $name, $this->mNamedDB ) ) {
$this->profileDBIn();
- $this->mNamedDB[$name] = wfGetDB($db, $groups);
+ $this->mNamedDB[$name] = wfGetDB( $db, $groups );
$this->profileDBOut();
}
return $this->mNamedDB[$name];
@@ -168,15 +169,15 @@ class ApiQuery extends ApiBase {
* @return array(modulename => classname)
*/
function getModules() {
- return array_merge($this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules);
+ return array_merge( $this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules );
}
-
+
public function getCustomPrinter() {
// If &exportnowrap is set, use the raw formatter
- if ($this->getParameter('export') &&
- $this->getParameter('exportnowrap'))
- return new ApiFormatRaw($this->getMain(),
- $this->getMain()->createPrinterByName('xml'));
+ if ( $this->getParameter( 'export' ) &&
+ $this->getParameter( 'exportnowrap' ) )
+ return new ApiFormatRaw( $this->getMain(),
+ $this->getMain()->createPrinterByName( 'xml' ) );
else
return null;
}
@@ -196,24 +197,18 @@ class ApiQuery extends ApiBase {
$this->params = $this->extractRequestParams();
$this->redirects = $this->params['redirects'];
- //
// Create PageSet
- //
- $this->mPageSet = new ApiPageSet($this, $this->redirects);
+ $this->mPageSet = new ApiPageSet( $this, $this->redirects );
- //
// Instantiate requested modules
- //
$modules = array ();
- $this->InstantiateModules($modules, 'prop', $this->mQueryPropModules);
- $this->InstantiateModules($modules, 'list', $this->mQueryListModules);
- $this->InstantiateModules($modules, 'meta', $this->mQueryMetaModules);
+ $this->InstantiateModules( $modules, 'prop', $this->mQueryPropModules );
+ $this->InstantiateModules( $modules, 'list', $this->mQueryListModules );
+ $this->InstantiateModules( $modules, 'meta', $this->mQueryMetaModules );
$cacheMode = 'public';
- //
// If given, execute generator to substitute user supplied data with generated data.
- //
if ( isset ( $this->params['generator'] ) ) {
$generator = $this->newGenerator( $this->params['generator'] );
$params = $generator->extractRequestParams();
@@ -222,25 +217,21 @@ class ApiQuery extends ApiBase {
$this->executeGeneratorModule( $generator, $modules );
} else {
// Append custom fields and populate page/revision information
- $this->addCustomFldsToPageSet($modules, $this->mPageSet);
+ $this->addCustomFldsToPageSet( $modules, $this->mPageSet );
$this->mPageSet->execute();
}
- //
// Record page information (title, namespace, if exists, etc)
- //
$this->outputGeneralPageInfo();
- //
// Execute all requested modules.
- //
- foreach ($modules as $module) {
+ foreach ( $modules as $module ) {
$params = $module->extractRequestParams();
$cacheMode = $this->mergeCacheMode(
$cacheMode, $module->getCacheMode( $params ) );
$module->profileIn();
$module->execute();
- wfRunHooks('APIQueryAfterExecute', array(&$module));
+ wfRunHooks( 'APIQueryAfterExecute', array( &$module ) );
$module->profileOut();
}
@@ -273,10 +264,10 @@ class ApiQuery extends ApiBase {
* @param $modules array of module objects
* @param $pageSet ApiPageSet
*/
- private function addCustomFldsToPageSet($modules, $pageSet) {
+ private function addCustomFldsToPageSet( $modules, $pageSet ) {
// Query all requested modules.
- foreach ($modules as $module) {
- $module->requestExtraData($pageSet);
+ foreach ( $modules as $module ) {
+ $module->requestExtraData( $pageSet );
}
}
@@ -286,11 +277,11 @@ class ApiQuery extends ApiBase {
* @param $param string Parameter name to read modules from
* @param $moduleList array(modulename => classname)
*/
- private function InstantiateModules(&$modules, $param, $moduleList) {
+ private function InstantiateModules( &$modules, $param, $moduleList ) {
$list = @$this->params[$param];
- if (!is_null ($list))
- foreach ($list as $moduleName)
- $modules[] = new $moduleList[$moduleName] ($this, $moduleName);
+ if ( !is_null ( $list ) )
+ foreach ( $list as $moduleName )
+ $modules[] = new $moduleList[$moduleName] ( $this, $moduleName );
}
/**
@@ -303,65 +294,65 @@ class ApiQuery extends ApiBase {
$pageSet = $this->getPageSet();
$result = $this->getResult();
- # We don't check for a full result set here because we can't be adding
- # more than 380K. The maximum revision size is in the megabyte range,
- # and the maximum result size must be even higher than that.
+ // We don't check for a full result set here because we can't be adding
+ // more than 380K. The maximum revision size is in the megabyte range,
+ // and the maximum result size must be even higher than that.
// Title normalizations
$normValues = array ();
- foreach ($pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr) {
+ foreach ( $pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
$normValues[] = array (
'from' => $rawTitleStr,
'to' => $titleStr
);
}
- if (count($normValues)) {
- $result->setIndexedTagName($normValues, 'n');
- $result->addValue('query', 'normalized', $normValues);
+ if ( count( $normValues ) ) {
+ $result->setIndexedTagName( $normValues, 'n' );
+ $result->addValue( 'query', 'normalized', $normValues );
}
// Interwiki titles
$intrwValues = array ();
- foreach ($pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr) {
+ foreach ( $pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) {
$intrwValues[] = array (
'title' => $rawTitleStr,
'iw' => $interwikiStr
);
}
- if (count($intrwValues)) {
- $result->setIndexedTagName($intrwValues, 'i');
- $result->addValue('query', 'interwiki', $intrwValues);
+ if ( count( $intrwValues ) ) {
+ $result->setIndexedTagName( $intrwValues, 'i' );
+ $result->addValue( 'query', 'interwiki', $intrwValues );
}
// Show redirect information
$redirValues = array ();
- foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) {
+ foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo ) {
$redirValues[] = array (
- 'from' => strval($titleStrFrom),
+ 'from' => strval( $titleStrFrom ),
'to' => $titleStrTo
);
}
- if (count($redirValues)) {
- $result->setIndexedTagName($redirValues, 'r');
- $result->addValue('query', 'redirects', $redirValues);
+ if ( count( $redirValues ) ) {
+ $result->setIndexedTagName( $redirValues, 'r' );
+ $result->addValue( 'query', 'redirects', $redirValues );
}
//
// Missing revision elements
//
$missingRevIDs = $pageSet->getMissingRevisionIDs();
- if (count($missingRevIDs)) {
+ if ( count( $missingRevIDs ) ) {
$revids = array ();
- foreach ($missingRevIDs as $revid) {
+ foreach ( $missingRevIDs as $revid ) {
$revids[$revid] = array (
'revid' => $revid
);
}
- $result->setIndexedTagName($revids, 'rev');
- $result->addValue('query', 'badrevids', $revids);
+ $result->setIndexedTagName( $revids, 'rev' );
+ $result->addValue( 'query', 'badrevids', $revids );
}
//
@@ -370,17 +361,17 @@ class ApiQuery extends ApiBase {
$pages = array ();
// Report any missing titles
- foreach ($pageSet->getMissingTitles() as $fakeId => $title) {
+ foreach ( $pageSet->getMissingTitles() as $fakeId => $title ) {
$vals = array();
- ApiQueryBase :: addTitleInfo($vals, $title);
+ ApiQueryBase :: addTitleInfo( $vals, $title );
$vals['missing'] = '';
$pages[$fakeId] = $vals;
}
// Report any invalid titles
- foreach ($pageSet->getInvalidTitles() as $fakeId => $title)
- $pages[$fakeId] = array('title' => $title, 'invalid' => '');
+ foreach ( $pageSet->getInvalidTitles() as $fakeId => $title )
+ $pages[$fakeId] = array( 'title' => $title, 'invalid' => '' );
// Report any missing page ids
- foreach ($pageSet->getMissingPageIDs() as $pageid) {
+ foreach ( $pageSet->getMissingPageIDs() as $pageid ) {
$pages[$pageid] = array (
'pageid' => $pageid,
'missing' => ''
@@ -388,51 +379,52 @@ class ApiQuery extends ApiBase {
}
// Output general page information for found titles
- foreach ($pageSet->getGoodTitles() as $pageid => $title) {
+ foreach ( $pageSet->getGoodTitles() as $pageid => $title ) {
$vals = array();
$vals['pageid'] = $pageid;
- ApiQueryBase :: addTitleInfo($vals, $title);
+ ApiQueryBase :: addTitleInfo( $vals, $title );
$pages[$pageid] = $vals;
}
- if (count($pages)) {
+ if ( count( $pages ) ) {
- if ($this->params['indexpageids']) {
- $pageIDs = array_keys($pages);
+ if ( $this->params['indexpageids'] ) {
+ $pageIDs = array_keys( $pages );
// json treats all map keys as strings - converting to match
- $pageIDs = array_map('strval', $pageIDs);
- $result->setIndexedTagName($pageIDs, 'id');
- $result->addValue('query', 'pageids', $pageIDs);
+ $pageIDs = array_map( 'strval', $pageIDs );
+ $result->setIndexedTagName( $pageIDs, 'id' );
+ $result->addValue( 'query', 'pageids', $pageIDs );
}
- $result->setIndexedTagName($pages, 'page');
- $result->addValue('query', 'pages', $pages);
+ $result->setIndexedTagName( $pages, 'page' );
+ $result->addValue( 'query', 'pages', $pages );
}
- if ($this->params['export']) {
- $exporter = new WikiExporter($this->getDB());
+ if ( $this->params['export'] ) {
+ $exporter = new WikiExporter( $this->getDB() );
// WikiExporter writes to stdout, so catch its
// output with an ob
ob_start();
$exporter->openStream();
- foreach (@$pageSet->getGoodTitles() as $title)
- if ($title->userCanRead())
- $exporter->pageByTitle($title);
+ foreach ( @$pageSet->getGoodTitles() as $title )
+ if ( $title->userCanRead() )
+ $exporter->pageByTitle( $title );
$exporter->closeStream();
$exportxml = ob_get_contents();
ob_end_clean();
+
// Don't check the size of exported stuff
// It's not continuable, so it would cause more
// problems than it'd solve
$result->disableSizeCheck();
- if ($this->params['exportnowrap']) {
+ if ( $this->params['exportnowrap'] ) {
$result->reset();
// Raw formatter will handle this
- $result->addValue(null, 'text', $exportxml);
- $result->addValue(null, 'mime', 'text/xml');
+ $result->addValue( null, 'text', $exportxml );
+ $result->addValue( null, 'mime', 'text/xml' );
} else {
$r = array();
- ApiResult::setContent($r, $exportxml);
- $result->addValue('query', 'export', $r);
+ ApiResult::setContent( $r, $exportxml );
+ $result->addValue( 'query', 'export', $r );
}
$result->enableSizeCheck();
}
@@ -442,22 +434,23 @@ class ApiQuery extends ApiBase {
* Create a generator object of the given type and return it
*/
public function newGenerator( $generatorName ) {
+
// Find class that implements requested generator
- if (isset ($this->mQueryListModules[$generatorName])) {
+ if ( isset ( $this->mQueryListModules[$generatorName] ) ) {
$className = $this->mQueryListModules[$generatorName];
- } elseif (isset ($this->mQueryPropModules[$generatorName])) {
+ } elseif ( isset ( $this->mQueryPropModules[$generatorName] ) ) {
$className = $this->mQueryPropModules[$generatorName];
} else {
- ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName");
+ ApiBase :: dieDebug( __METHOD__, "Unknown generator=$generatorName" );
}
// Generator results
- $resultPageSet = new ApiPageSet($this, $this->redirects);
+ $resultPageSet = new ApiPageSet( $this, $this->redirects );
// Create and execute the generator
- $generator = new $className ($this, $generatorName);
- if (!$generator instanceof ApiQueryGeneratorBase)
- $this->dieUsage("Module $generatorName cannot be used as a generator", "badgenerator");
+ $generator = new $className ( $this, $generatorName );
+ if ( !$generator instanceof ApiQueryGeneratorBase )
+ $this->dieUsage( "Module $generatorName cannot be used as a generator", "badgenerator" );
$generator->setGeneratorMode();
return $generator;
}
@@ -473,16 +466,16 @@ class ApiQuery extends ApiBase {
$resultPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles );
// Add any additional fields modules may need
- $generator->requestExtraData($this->mPageSet);
- $this->addCustomFldsToPageSet($modules, $resultPageSet);
+ $generator->requestExtraData( $this->mPageSet );
+ $this->addCustomFldsToPageSet( $modules, $resultPageSet );
// Populate page information with the original user input
$this->mPageSet->execute();
// populate resultPageSet with the generator output
$generator->profileIn();
- $generator->executeGenerator($resultPageSet);
- wfRunHooks('APIQueryGeneratorAfterExecute', array(&$generator, &$resultPageSet));
+ $generator->executeGenerator( $resultPageSet );
+ wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$resultPageSet ) );
$resultPageSet->finishPageSetGeneration();
$generator->profileOut();
@@ -527,14 +520,14 @@ class ApiQuery extends ApiBase {
$this->mPageSet = null;
$this->mAllowedGenerators = array(); // Will be repopulated
- $astriks = str_repeat('--- ', 8);
- $astriks2 = str_repeat('*** ', 10);
+ $astriks = str_repeat( '--- ', 8 );
+ $astriks2 = str_repeat( '*** ', 10 );
$msg .= "\n$astriks Query: Prop $astriks\n\n";
- $msg .= $this->makeHelpMsgHelper($this->mQueryPropModules, 'prop');
+ $msg .= $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' );
$msg .= "\n$astriks Query: List $astriks\n\n";
- $msg .= $this->makeHelpMsgHelper($this->mQueryListModules, 'list');
+ $msg .= $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' );
$msg .= "\n$astriks Query: Meta $astriks\n\n";
- $msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta');
+ $msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' );
$msg .= "\n\n$astriks2 Modules: continuation $astriks2\n\n";
// Perform the base call last because the $this->mAllowedGenerators
@@ -551,25 +544,25 @@ class ApiQuery extends ApiBase {
* @param $paramName string Parameter name
* @return string
*/
- private function makeHelpMsgHelper($moduleList, $paramName) {
+ private function makeHelpMsgHelper( $moduleList, $paramName ) {
$moduleDescriptions = array ();
- foreach ($moduleList as $moduleName => $moduleClass) {
- $module = new $moduleClass ($this, $moduleName, null);
+ foreach ( $moduleList as $moduleName => $moduleClass ) {
+ $module = new $moduleClass ( $this, $moduleName, null );
- $msg = ApiMain::makeHelpMsgHeader($module, $paramName);
+ $msg = ApiMain::makeHelpMsgHeader( $module, $paramName );
$msg2 = $module->makeHelpMsg();
- if ($msg2 !== false)
+ if ( $msg2 !== false )
$msg .= $msg2;
- if ($module instanceof ApiQueryGeneratorBase) {
+ if ( $module instanceof ApiQueryGeneratorBase ) {
$this->mAllowedGenerators[] = $moduleName;
$msg .= "Generator:\n This module may be used as a generator\n";
}
$moduleDescriptions[] = $msg;
}
- return implode("\n", $moduleDescriptions);
+ return implode( "\n", $moduleDescriptions );
}
/**
@@ -577,7 +570,7 @@ class ApiQuery extends ApiBase {
* @return string
*/
public function makeHelpMsgParameters() {
- $psModule = new ApiPageSet($this);
+ $psModule = new ApiPageSet( $this );
return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters();
}
@@ -590,8 +583,8 @@ class ApiQuery extends ApiBase {
'prop' => 'Which properties to get for the titles/revisions/pageids',
'list' => 'Which lists to get',
'meta' => 'Which meta data to get about the site',
- 'generator' => array('Use the output of a list as the input for other prop/list/meta items',
- 'NOTE: generator parameter names must be prefixed with a \'g\', see examples.'),
+ 'generator' => array( 'Use the output of a list as the input for other prop/list/meta items',
+ 'NOTE: generator parameter names must be prefixed with a \'g\', see examples.' ),
'redirects' => 'Automatically resolve redirects',
'indexpageids' => 'Include an additional pageids section listing all returned page IDs.',
'export' => 'Export the current revisions of all given or generated pages',
@@ -606,6 +599,12 @@ class ApiQuery extends ApiBase {
'All data modifications will first have to use query to acquire a token to prevent abuse from malicious sites.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -615,9 +614,9 @@ class ApiQuery extends ApiBase {
}
public function getVersion() {
- $psModule = new ApiPageSet($this);
+ $psModule = new ApiPageSet( $this );
$vers = array ();
- $vers[] = __CLASS__ . ': $Id: ApiQuery.php 69986 2010-07-27 03:57:39Z tstarling $';
+ $vers[] = __CLASS__ . ': $Id: ApiQuery.php 69932 2010-07-26 08:03:21Z tstarling $';
$vers[] = $psModule->getVersion();
return $vers;
}
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
index fca92c4b..8f24fc7c 100644
--- a/includes/api/ApiQueryAllCategories.php
+++ b/includes/api/ApiQueryAllCategories.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -36,8 +36,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryAllCategories extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'ac');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'ac' );
}
public function execute() {
@@ -48,86 +48,86 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
$db = $this->getDB();
$params = $this->extractRequestParams();
- $this->addTables('category');
- $this->addFields('cat_title');
+ $this->addTables( 'category' );
+ $this->addFields( 'cat_title' );
- $dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
- $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
- $this->addWhereRange('cat_title', $dir, $from, null);
- if (isset ($params['prefix']))
- $this->addWhere("cat_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
+ $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
+ $this->addWhereRange( 'cat_title', $dir, $from, null );
+ if ( isset ( $params['prefix'] ) )
+ $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
- $this->addOption('LIMIT', $params['limit']+1);
- $this->addOption('ORDER BY', 'cat_title' . ($params['dir'] == 'descending' ? ' DESC' : ''));
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $this->addOption( 'ORDER BY', 'cat_title' . ( $params['dir'] == 'descending' ? ' DESC' : '' ) );
- $prop = array_flip($params['prop']);
- $this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset($prop['size']) );
- if(isset($prop['hidden']))
+ $prop = array_flip( $params['prop'] );
+ $this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset( $prop['size'] ) );
+ if ( isset( $prop['hidden'] ) )
{
- $this->addTables(array('page', 'page_props'));
- $this->addJoinConds(array(
- 'page' => array('LEFT JOIN', array(
+ $this->addTables( array( 'page', 'page_props' ) );
+ $this->addJoinConds( array(
+ 'page' => array( 'LEFT JOIN', array(
'page_namespace' => NS_CATEGORY,
- 'page_title=cat_title')),
- 'page_props' => array('LEFT JOIN', array(
+ 'page_title=cat_title' ) ),
+ 'page_props' => array( 'LEFT JOIN', array(
'pp_page=page_id',
- 'pp_propname' => 'hiddencat')),
- ));
- $this->addFields('pp_propname AS cat_hidden');
+ 'pp_propname' => 'hiddencat' ) ),
+ ) );
+ $this->addFields( 'pp_propname AS cat_hidden' );
}
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$pages = array();
$categories = array();
$result = $this->getResult();
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $params['limit']) {
+ 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', $this->keyToTitle($row->cat_title));
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) );
break;
}
// Normalize titles
- $titleObj = Title::makeTitle(NS_CATEGORY, $row->cat_title);
- if(!is_null($resultPageSet))
+ $titleObj = Title::makeTitle( NS_CATEGORY, $row->cat_title );
+ if ( !is_null( $resultPageSet ) )
$pages[] = $titleObj->getPrefixedText();
else {
$item = array();
$result->setContent( $item, $titleObj->getText() );
- if( isset( $prop['size'] ) ) {
- $item['size'] = intval($row->cat_pages);
+ if ( isset( $prop['size'] ) ) {
+ $item['size'] = intval( $row->cat_pages );
$item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files;
- $item['files'] = intval($row->cat_files);
- $item['subcats'] = intval($row->cat_subcats);
+ $item['files'] = intval( $row->cat_files );
+ $item['subcats'] = intval( $row->cat_subcats );
}
- if( isset( $prop['hidden'] ) && $row->cat_hidden )
+ if ( isset( $prop['hidden'] ) && $row->cat_hidden )
$item['hidden'] = '';
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $item);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $item );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('from', $this->keyToTitle($row->cat_title));
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) );
break;
}
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'c');
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'c' );
} else {
- $resultPageSet->populateFromTitles($pages);
+ $resultPageSet->populateFromTitles( $pages );
}
}
@@ -179,6 +179,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllCategories.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryAllCategories.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index 73788aa6..6b6fc2c0 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryAllLinks extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'al');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'al' );
}
public function execute() {
@@ -47,105 +47,105 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
$db = $this->getDB();
$params = $this->extractRequestParams();
- $prop = array_flip($params['prop']);
- $fld_ids = isset($prop['ids']);
- $fld_title = isset($prop['title']);
+ $prop = array_flip( $params['prop'] );
+ $fld_ids = isset( $prop['ids'] );
+ $fld_title = isset( $prop['title'] );
- if ($params['unique']) {
- if (!is_null($resultPageSet))
- $this->dieUsage($this->getModuleName() . ' cannot be used as a generator in unique links mode', 'params');
- if ($fld_ids)
- $this->dieUsage($this->getModuleName() . ' cannot return corresponding page ids in unique links mode', 'params');
- $this->addOption('DISTINCT');
+ if ( $params['unique'] ) {
+ if ( !is_null( $resultPageSet ) )
+ $this->dieUsage( $this->getModuleName() . ' cannot be used as a generator in unique links mode', 'params' );
+ if ( $fld_ids )
+ $this->dieUsage( $this->getModuleName() . ' cannot return corresponding page ids in unique links mode', 'params' );
+ $this->addOption( 'DISTINCT' );
}
- $this->addTables('pagelinks');
- $this->addWhereFld('pl_namespace', $params['namespace']);
+ $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']))
+ 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');
- $from = $this->getDB()->strencode($this->titleToKey($arr[0]));
- $id = intval($arr[1]);
- $this->addWhere("pl_title > '$from' OR " .
+ $arr = explode( '|', $params['continue'] );
+ if ( count( $arr ) != 2 )
+ $this->dieUsage( "Invalid continue parameter", 'badcontinue' );
+ $from = $this->getDB()->strencode( $this->titleToKey( $arr[0] ) );
+ $id = intval( $arr[1] );
+ $this->addWhere( "pl_title > '$from' OR " .
"(pl_title = '$from' AND " .
- "pl_from > $id)");
- }
+ "pl_from > $id)" );
+ }
- if (!is_null($params['from']))
- $this->addWhere('pl_title>=' . $db->addQuotes($this->titlePartToKey($params['from'])));
- if (isset ($params['prefix']))
- $this->addWhere("pl_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
+ if ( !is_null( $params['from'] ) )
+ $this->addWhere( 'pl_title>=' . $db->addQuotes( $this->titlePartToKey( $params['from'] ) ) );
+ if ( isset ( $params['prefix'] ) )
+ $this->addWhere( 'pl_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
- $this->addFields(array (
+ $this->addFields( array (
'pl_title',
- ));
- $this->addFieldsIf('pl_from', !$params['unique']);
+ ) );
+ $this->addFieldsIf( 'pl_from', !$params['unique'] );
- $this->addOption('USE INDEX', 'pl_namespace');
+ $this->addOption( 'USE INDEX', 'pl_namespace' );
$limit = $params['limit'];
- $this->addOption('LIMIT', $limit+1);
- if($params['unique'])
- $this->addOption('ORDER BY', 'pl_title');
+ $this->addOption( 'LIMIT', $limit + 1 );
+ if ( $params['unique'] )
+ $this->addOption( 'ORDER BY', 'pl_title' );
else
- $this->addOption('ORDER BY', 'pl_title, pl_from');
+ $this->addOption( 'ORDER BY', 'pl_title, pl_from' );
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$pageids = array ();
$count = 0;
$result = $this->getResult();
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ 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
- if($params['unique'])
- $this->setContinueEnumParameter('from', $this->keyToTitle($row->pl_title));
+ if ( $params['unique'] )
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) );
else
- $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from);
+ $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from );
break;
}
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$vals = array();
- if ($fld_ids)
- $vals['fromid'] = intval($row->pl_from);
- if ($fld_title) {
- $title = Title :: makeTitle($params['namespace'], $row->pl_title);
- ApiQueryBase::addTitleInfo($vals, $title);
+ if ( $fld_ids )
+ $vals['fromid'] = intval( $row->pl_from );
+ if ( $fld_title ) {
+ $title = Title :: makeTitle( $params['namespace'], $row->pl_title );
+ ApiQueryBase::addTitleInfo( $vals, $title );
}
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- if($params['unique'])
- $this->setContinueEnumParameter('from', $this->keyToTitle($row->pl_title));
+ if ( $params['unique'] )
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) );
else
- $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from);
+ $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from );
break;
}
} else {
$pageids[] = $row->pl_from;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'l');
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'l' );
} else {
- $resultPageSet->populateFromPageIDs($pageids);
+ $resultPageSet->populateFromPageIDs( $pageids );
}
}
@@ -192,6 +192,15 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
public function getDescription() {
return 'Enumerate all links that point to a given namespace';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'params', 'info' => $this->getModuleName() . ' cannot be used as a generator in unique links mode' ),
+ array( 'code' => 'params', 'info' => $this->getModuleName() . ' cannot return corresponding page ids in unique links mode' ),
+ array( 'code' => 'params', 'info' => 'alcontinue and alfrom cannot be used together' ),
+ array( 'code' => 'badcontinue', 'info' => 'Invalid continue parameter' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -200,6 +209,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllLinks.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryAllLinks.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index c18f6dd1..f8d475cc 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryAllUsers extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'au');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'au' );
}
public function execute() {
@@ -44,67 +44,74 @@ class ApiQueryAllUsers extends ApiQueryBase {
$params = $this->extractRequestParams();
$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 {
+ 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_blockinfo = $fld_editcount = $fld_groups = $fld_registration = false;
}
$limit = $params['limit'];
- $this->addTables('user', 'u1');
+ $this->addTables( 'user', 'u1' );
+ $useIndex = true;
- if (!is_null($params['from']))
- $this->addWhere('u1.user_name >= ' . $db->addQuotes($this->keyToTitle($params['from'])));
+ if ( !is_null( $params['from'] ) )
+ $this->addWhere( 'u1.user_name >= ' . $db->addQuotes( $this->keyToTitle( $params['from'] ) ) );
- if (!is_null($params['prefix']))
- $this->addWhere('u1.user_name LIKE "' . $db->escapeLike($this->keyToTitle( $params['prefix'])) . '%"');
+ if ( !is_null( $params['prefix'] ) )
+ $this->addWhere( 'u1.user_name' . $db->buildLike( $this->keyToTitle( $params['prefix'] ), $db->anyString() ) );
- if (!is_null($params['group'])) {
+ if ( !is_null( $params['group'] ) ) {
+ $useIndex = false;
// Filter only users that belong to a given group
- $this->addTables('user_groups', 'ug1');
- $this->addWhere('ug1.ug_user=u1.user_id');
- $this->addWhereFld('ug1.ug_group', $params['group']);
+ $this->addTables( 'user_groups', 'ug1' );
+ $ug1 = $this->getAliasedName( 'user_groups', 'ug1' );
+ $this->addJoinConds( array( $ug1 => array( 'INNER JOIN', array( 'ug1.ug_user=u1.user_id',
+ 'ug1.ug_group' => $params['group'] ) ) ) );
}
- if ($params['witheditsonly'])
- $this->addWhere('u1.user_editcount > 0');
+ if ( $params['witheditsonly'] )
+ $this->addWhere( 'u1.user_editcount > 0' );
- if ($fld_groups) {
+ if ( $fld_groups ) {
// Show the groups the given users belong to
// request more than needed to avoid not getting all rows that belong to one user
- $groupCount = count(User::getAllGroups());
- $sqlLimit = $limit+$groupCount+1;
+ $groupCount = count( User::getAllGroups() );
+ $sqlLimit = $limit + $groupCount + 1;
- $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');
+ $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;
+ $sqlLimit = $limit + 1;
}
- 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'));
+ 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 AS blocker_name' ) );
}
- $this->addOption('LIMIT', $sqlLimit);
+ $this->addOption( 'LIMIT', $sqlLimit );
- $this->addFields('u1.user_name');
- $this->addFieldsIf('u1.user_editcount', $fld_editcount);
- $this->addFieldsIf('u1.user_registration', $fld_registration);
+ $this->addFields( 'u1.user_name' );
+ $this->addFieldsIf( 'u1.user_editcount', $fld_editcount );
+ $this->addFieldsIf( 'u1.user_registration', $fld_registration );
- $this->addOption('ORDER BY', 'u1.user_name');
+ $this->addOption( 'ORDER BY', 'u1.user_name' );
+ if ( $useIndex ) {
+ $u1 = $this->getAliasedName( 'user', 'u1' );
+ $this->addOption( 'USE INDEX', array( $u1 => 'user_name' ) );
+ }
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$data = array ();
$count = 0;
@@ -119,66 +126,67 @@ class ApiQueryAllUsers extends ApiQueryBase {
// The setContinue... is more complex because of this, and takes into account the higher sql limit
// to make sure all rows that belong to the same user are received.
//
- while (true) {
+ while ( true ) {
- $row = $db->fetchObject($res);
+ $row = $db->fetchObject( $res );
$count++;
- if (!$row || $lastUser !== $row->user_name) {
+ if ( !$row || $lastUser !== $row->user_name ) {
// Save the last pass's user data
- if (is_array($lastUserData))
+ if ( is_array( $lastUserData ) )
{
- $fit = $result->addValue(array('query', $this->getModuleName()),
- null, $lastUserData);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ),
+ null, $lastUserData );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('from',
- $this->keyToTitle($lastUserData['name']));
+ $this->setContinueEnumParameter( 'from',
+ $this->keyToTitle( $lastUserData['name'] ) );
break;
}
}
// No more rows left
- if (!$row)
+ if ( !$row )
break;
- if ($count > $limit) {
+ if ( $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('from', $this->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) {
+ 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 ( $fld_editcount )
+ $lastUserData['editcount'] = intval( $row->user_editcount );
+ if ( $fld_registration )
+ $lastUserData['registration'] = $row->user_registration ?
+ wfTimestamp( TS_ISO_8601, $row->user_registration ) : '';
}
- if ($sqlLimit == $count) {
+ if ( $sqlLimit == $count ) {
// BUG! database contains group name that User::getAllGroups() does not return
// TODO: should handle this more gracefully
- ApiBase :: dieDebug(__METHOD__,
- 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function');
+ 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)) {
+ if ( $fld_groups && !is_null( $row->ug_group2 ) ) {
$lastUserData['groups'][] = $row->ug_group2;
- $result->setIndexedTagName($lastUserData['groups'], 'g');
+ $result->setIndexedTagName( $lastUserData['groups'], 'g' );
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'u');
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'u' );
}
public function getCacheMode( $params ) {
@@ -219,7 +227,7 @@ class ApiQueryAllUsers extends ApiQueryBase {
'group' => 'Limit users to a given group name',
'prop' => array(
'What pieces of information to include.',
- '`groups` property uses more server resources and may return fewer results than the limit.'),
+ '`groups` property uses more server resources and may return fewer results than the limit.' ),
'limit' => 'How many total user names to return.',
'witheditsonly' => 'Only list users who have made edits',
);
@@ -236,6 +244,6 @@ class ApiQueryAllUsers extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllUsers.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryAllUsers.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php
index 983c6469..0a745516 100644
--- a/includes/api/ApiQueryAllimages.php
+++ b/includes/api/ApiQueryAllimages.php
@@ -24,9 +24,9 @@
* 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' );
}
/**
@@ -36,8 +36,19 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryAllimages extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'ai');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'ai' );
+ $this->mRepo = RepoGroup::singleton()->getLocalRepo();
+ }
+
+ /**
+ * Overide parent method to make sure to make sure the repo's DB is used
+ * which may not necesarilly be the same as the local DB.
+ *
+ * TODO: allow querying non-local repos.
+ */
+ protected function getDB() {
+ return $this->mRepo->getSlaveDB();
}
public function execute() {
@@ -48,89 +59,89 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- if ($resultPageSet->isResolvingRedirects())
- $this->dieUsage('Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params');
+ 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);
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
- $repo = RepoGroup::singleton()->getLocalRepo();
+ private function run( $resultPageSet = null ) {
+ $repo = $this->mRepo;
if ( !$repo instanceof LocalRepo )
- $this->dieUsage('Local file repository does not support querying all images', 'unsupportedrepo');
+ $this->dieUsage( 'Local file repository does not support querying all images', 'unsupportedrepo' );
$db = $this->getDB();
$params = $this->extractRequestParams();
// Image filters
- $dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
- $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
- $this->addWhereRange('img_name', $dir, $from, null);
- if (isset ($params['prefix']))
- $this->addWhere("img_name LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
-
- if (isset ($params['minsize'])) {
- $this->addWhere('img_size>=' . intval($params['minsize']));
+ $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
+ $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
+ $this->addWhereRange( 'img_name', $dir, $from, null );
+ if ( isset ( $params['prefix'] ) )
+ $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
+
+ if ( isset ( $params['minsize'] ) ) {
+ $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) );
}
- if (isset ($params['maxsize'])) {
- $this->addWhere('img_size<=' . intval($params['maxsize']));
+ if ( isset ( $params['maxsize'] ) ) {
+ $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) );
}
$sha1 = false;
- if( isset( $params['sha1'] ) ) {
+ if ( isset( $params['sha1'] ) ) {
$sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 );
- } elseif( isset( $params['sha1base36'] ) ) {
+ } elseif ( isset( $params['sha1base36'] ) ) {
$sha1 = $params['sha1base36'];
}
- if( $sha1 ) {
+ if ( $sha1 ) {
$this->addWhere( 'img_sha1=' . $db->addQuotes( $sha1 ) );
}
- $this->addTables('image');
+ $this->addTables( 'image' );
- $prop = array_flip($params['prop']);
+ $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' : ''));
+ $this->addOption( 'LIMIT', $limit + 1 );
+ $this->addOption( 'ORDER BY', 'img_name' .
+ ( $params['dir'] == 'descending' ? ' DESC' : '' ) );
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$titles = array();
$count = 0;
$result = $this->getResult();
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ 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));
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
break;
}
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$file = $repo->newFileFromRow( $row );
- $info = array_merge(array('name' => $row->img_name),
- ApiQueryImageInfo::getInfo($file, $prop, $result));
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $info);
- if( !$fit ) {
- $this->setContinueEnumParameter('from', $this->keyToTitle($row->img_name));
+ $info = array_merge( array( 'name' => $row->img_name ),
+ ApiQueryImageInfo::getInfo( $file, $prop, $result ) );
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) );
break;
}
} else {
- $titles[] = Title::makeTitle(NS_IMAGE, $row->img_name);
+ $titles[] = Title::makeTitle( NS_IMAGE, $row->img_name );
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'img');
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' );
} else {
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
}
@@ -161,18 +172,7 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
'sha1' => null,
'sha1base36' => null,
'prop' => array (
- ApiBase :: PARAM_TYPE => array(
- 'timestamp',
- 'user',
- 'comment',
- 'url',
- 'size',
- 'dimensions', // Obsolete
- 'mime',
- 'sha1',
- 'metadata',
- 'bitdepth',
- ),
+ ApiBase :: PARAM_TYPE => ApiQueryImageInfo::getPropertyNames(),
ApiBase :: PARAM_DFLT => 'timestamp|url',
ApiBase :: PARAM_ISMULTI => true
)
@@ -196,6 +196,13 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
public function getDescription() {
return 'Enumerate all images sequentially';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'params', 'info' => 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator' ),
+ array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -209,6 +216,6 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllimages.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryAllimages.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllmessages.php
index c615daf4..7dd9d874 100644
--- a/includes/api/ApiQueryAllmessages.php
+++ b/includes/api/ApiQueryAllmessages.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,79 +35,98 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryAllmessages extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'am');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'am' );
}
public function execute() {
- global $wgMessageCache;
$params = $this->extractRequestParams();
- if(!is_null($params['lang']))
+ if ( !is_null( $params['lang'] ) )
{
global $wgLang;
- $wgLang = Language::factory($params['lang']);
+ $wgLang = Language::factory( $params['lang'] );
}
+
+ $prop = array_flip( (array)$params['prop'] );
-
- //Determine which messages should we print
+ // Determine which messages should we print
$messages_target = array();
- if( $params['messages'] == '*' ) {
- $wgMessageCache->loadAllMessages();
- $message_names = array_keys( array_merge( Language::getMessagesFor( 'en' ), $wgMessageCache->getExtensionMessagesFor( 'en' ) ) );
+ if ( in_array( '*', $params['messages'] ) ) {
+ $message_names = array_keys( Language::getMessagesFor( 'en' ) );
sort( $message_names );
$messages_target = $message_names;
} else {
- $messages_target = explode( '|', $params['messages'] );
+ $messages_target = $params['messages'];
}
- //Filter messages
- if( isset( $params['filter'] ) ) {
+ // Filter messages
+ if ( isset( $params['filter'] ) ) {
$messages_filtered = array();
- foreach( $messages_target as $message ) {
- if( strpos( $message, $params['filter'] ) !== false ) { //!== is used because filter can be at the beginnig of the string
+ foreach ( $messages_target as $message ) {
+ if ( strpos( $message, $params['filter'] ) !== false ) { // !== is used because filter can be at the beginnig of the string
$messages_filtered[] = $message;
}
}
$messages_target = $messages_filtered;
}
- //Get all requested messages
+ // Get all requested messages and print the result
$messages = array();
- $skip = !is_null($params['from']);
- foreach( $messages_target as $message ) {
+ $skip = !is_null( $params['from'] );
+ $result = $this->getResult();
+ foreach ( $messages_target as $message ) {
// Skip all messages up to $params['from']
- if($skip && $message === $params['from'])
+ if ( $skip && $message === $params['from'] )
$skip = false;
- if(!$skip)
- $messages[$message] = wfMsg( $message );
- }
- //Print the result
- $result = $this->getResult();
- $messages_out = array();
- foreach( $messages as $name => $value ) {
- $message = array();
- $message['name'] = $name;
- if( wfEmptyMsg( $name, $value ) ) {
- $message['missing'] = '';
- } else {
- $result->setContent( $message, $value );
- }
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $message);
- if(!$fit)
- {
- $this->setContinueEnumParameter('from', $name);
- break;
+ if ( !$skip ) {
+ $a = array( 'name' => $message );
+ $args = null;
+ if ( isset( $params['args'] ) && count( $params['args'] ) != 0 ) {
+ $args = $params['args'];
+ }
+ // Check if the parser is enabled:
+ if ( $params['enableparser'] ) {
+ $msg = wfMsgExt( $message, array( 'parsemag' ), $args );
+ } else if ( $args ) {
+ $msgString = wfMsgGetKey( $message, true, false, false );
+ $msg = wfMsgReplaceArgs( $msgString, $args );
+ } else {
+ $msg = wfMsgGetKey( $message, true, false, false );
+ }
+
+ if ( wfEmptyMsg( $message, $msg ) )
+ $a['missing'] = '';
+ else {
+ ApiResult::setContent( $a, $msg );
+ if ( isset( $prop['default'] ) ) {
+ $default = wfMsgGetKey( $message, false, false, false );
+ if ( $default !== $msg ) {
+ if ( wfEmptyMsg( $message, $default ) )
+ $a['defaultmissing'] = '';
+ else
+ $a['default'] = $default;
+ }
+ }
+ }
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $a );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'from', $name );
+ break;
+ }
}
}
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'message');
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'message' );
}
public function getCacheMode( $params ) {
if ( is_null( $params['lang'] ) ) {
// Language not specified, will be fetched from preferences
return 'anon-public-user-private';
+ } elseif ( $params['enableparser'] ) {
+ // User-specific parser options will be used
+ return 'anon-public-user-private';
} else {
// OK to cache
return 'public';
@@ -118,6 +137,17 @@ class ApiQueryAllmessages extends ApiQueryBase {
return array (
'messages' => array (
ApiBase :: PARAM_DFLT => '*',
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
+ 'prop' => array(
+ ApiBase :: PARAM_ISMULTI => true,
+ ApiBase :: PARAM_TYPE => array(
+ 'default'
+ )
+ ),
+ 'enableparser' => false,
+ 'args' => array(
+ ApiBase :: PARAM_ISMULTI => true
),
'filter' => array(),
'lang' => null,
@@ -128,6 +158,10 @@ class ApiQueryAllmessages extends ApiQueryBase {
public function getParamDescription() {
return array (
'messages' => 'Which messages to output. "*" means all messages',
+ 'prop' => 'Which properties to get',
+ 'enableparser' => array( 'Set to enable parser, will preprocess the wikitext of message',
+ 'Will substitute magic words, handle templates etc.' ),
+ 'args' => 'Arguments to be substituted into message',
'filter' => 'Return only messages that contain this string',
'lang' => 'Return messages in this language',
'from' => 'Return messages starting at this message',
@@ -146,6 +180,6 @@ class ApiQueryAllmessages extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllmessages.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryAllmessages.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php
index e123e8fe..37f22ee2 100644
--- a/includes/api/ApiQueryAllpages.php
+++ b/includes/api/ApiQueryAllpages.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryAllpages extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'ap');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'ap' );
}
public function execute() {
@@ -47,31 +47,35 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- if ($resultPageSet->isResolvingRedirects())
- $this->dieUsage('Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator', 'params');
+ public function executeGenerator( $resultPageSet ) {
+ if ( $resultPageSet->isResolvingRedirects() )
+ $this->dieUsage( 'Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator', 'params' );
- $this->run($resultPageSet);
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
-
+ private function run( $resultPageSet = null ) {
$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']);
- $dir = ($params['dir'] == 'descending' ? 'older' : 'newer');
- $from = (is_null($params['from']) ? null : $this->titlePartToKey($params['from']));
- $this->addWhereRange('page_title', $dir, $from, null);
- if (isset ($params['prefix']))
- $this->addWhere("page_title LIKE '" . $db->escapeLike($this->titlePartToKey($params['prefix'])) . "%'");
-
- if (is_null($resultPageSet)) {
+ $this->addTables( 'page' );
+
+ if ( $params['filterredir'] == 'redirects' )
+ $this->addWhereFld( 'page_is_redirect', 1 );
+ else if ( $params['filterredir'] == 'nonredirects' )
+ $this->addWhereFld( 'page_is_redirect', 0 );
+
+ $this->addWhereFld( 'page_namespace', $params['namespace'] );
+ $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' );
+ $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
+ $this->addWhereRange( 'page_title', $dir, $from, null );
+
+ if ( isset ( $params['prefix'] ) )
+ $this->addWhere( 'page_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
+
+ if ( is_null( $resultPageSet ) ) {
$selectFields = array (
'page_namespace',
'page_title',
@@ -80,95 +84,98 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
} else {
$selectFields = $resultPageSet->getPageTableFields();
}
- $this->addFields($selectFields);
+
+ $this->addFields( $selectFields );
$forceNameTitleIndex = true;
- if (isset ($params['minsize'])) {
- $this->addWhere('page_len>=' . intval($params['minsize']));
+ if ( isset ( $params['minsize'] ) ) {
+ $this->addWhere( 'page_len>=' . intval( $params['minsize'] ) );
$forceNameTitleIndex = false;
}
- if (isset ($params['maxsize'])) {
- $this->addWhere('page_len<=' . intval($params['maxsize']));
+ if ( isset ( $params['maxsize'] ) ) {
+ $this->addWhere( 'page_len<=' . intval( $params['maxsize'] ) );
$forceNameTitleIndex = false;
}
// Page protection filtering
- if (!empty ($params['prtype'])) {
- $this->addTables('page_restrictions');
- $this->addWhere('page_id=pr_page');
- $this->addWhere('pr_expiry>' . $db->addQuotes($db->timestamp()));
- $this->addWhereFld('pr_type', $params['prtype']);
-
- // Remove the empty string and '*' from the prlevel array
- $prlevel = array_diff($params['prlevel'], array('', '*'));
- if (!empty($prlevel))
- $this->addWhereFld('pr_level', $prlevel);
- if ($params['prfiltercascade'] == 'cascading')
- $this->addWhereFld('pr_cascade', 1);
- if ($params['prfiltercascade'] == 'noncascading')
- $this->addWhereFld('pr_cascade', 0);
-
- $this->addOption('DISTINCT');
+ if ( !empty ( $params['prtype'] ) ) {
+ $this->addTables( 'page_restrictions' );
+ $this->addWhere( 'page_id=pr_page' );
+ $this->addWhere( 'pr_expiry>' . $db->addQuotes( $db->timestamp() ) );
+ $this->addWhereFld( 'pr_type', $params['prtype'] );
+
+ if ( isset ( $params['prlevel'] ) ) {
+ // Remove the empty string and '*' from the prlevel array
+ $prlevel = array_diff( $params['prlevel'], array( '', '*' ) );
+
+ if ( !empty( $prlevel ) )
+ $this->addWhereFld( 'pr_level', $prlevel );
+ }
+ if ( $params['prfiltercascade'] == 'cascading' )
+ $this->addWhereFld( 'pr_cascade', 1 );
+ else if ( $params['prfiltercascade'] == 'noncascading' )
+ $this->addWhereFld( 'pr_cascade', 0 );
+
+ $this->addOption( 'DISTINCT' );
$forceNameTitleIndex = false;
- } else if (isset ($params['prlevel'])) {
- $this->dieUsage('prlevel may not be used without prtype', 'params');
+ } else if ( isset ( $params['prlevel'] ) ) {
+ $this->dieUsage( 'prlevel may not be used without prtype', 'params' );
}
- if($params['filterlanglinks'] == 'withoutlanglinks') {
- $this->addTables('langlinks');
- $this->addJoinConds(array('langlinks' => array('LEFT JOIN', 'page_id=ll_from')));
- $this->addWhere('ll_from IS NULL');
+ if ( $params['filterlanglinks'] == 'withoutlanglinks' ) {
+ $this->addTables( 'langlinks' );
+ $this->addJoinConds( array( 'langlinks' => array( 'LEFT JOIN', 'page_id=ll_from' ) ) );
+ $this->addWhere( 'll_from IS NULL' );
$forceNameTitleIndex = false;
- } else if($params['filterlanglinks'] == 'withlanglinks') {
- $this->addTables('langlinks');
- $this->addWhere('page_id=ll_from');
- $this->addOption('STRAIGHT_JOIN');
+ } else if ( $params['filterlanglinks'] == 'withlanglinks' ) {
+ $this->addTables( 'langlinks' );
+ $this->addWhere( 'page_id=ll_from' );
+ $this->addOption( 'STRAIGHT_JOIN' );
// We have to GROUP BY all selected fields to stop
// PostgreSQL from whining
- $this->addOption('GROUP BY', implode(', ', $selectFields));
+ $this->addOption( 'GROUP BY', implode( ', ', $selectFields ) );
$forceNameTitleIndex = false;
}
- if ($forceNameTitleIndex)
- $this->addOption('USE INDEX', 'name_title');
-
+ if ( $forceNameTitleIndex )
+ $this->addOption( 'USE INDEX', 'name_title' );
$limit = $params['limit'];
- $this->addOption('LIMIT', $limit+1);
- $res = $this->select(__METHOD__);
+ $this->addOption( 'LIMIT', $limit + 1 );
+ $res = $this->select( __METHOD__ );
$count = 0;
$result = $this->getResult();
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ 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->page_title));
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
break;
}
- if (is_null($resultPageSet)) {
- $title = Title :: makeTitle($row->page_namespace, $row->page_title);
+ if ( is_null( $resultPageSet ) ) {
+ $title = Title :: makeTitle( $row->page_namespace, $row->page_title );
$vals = array(
- 'pageid' => intval($row->page_id),
- 'ns' => intval($title->getNamespace()),
- 'title' => $title->getPrefixedText());
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ 'pageid' => intval( $row->page_id ),
+ 'ns' => intval( $title->getNamespace() ),
+ 'title' => $title->getPrefixedText() );
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('from', $this->keyToTitle($row->page_title));
+ $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) );
break;
}
} else {
- $resultPageSet->processDbRow($row);
+ $resultPageSet->processDbRow( $row );
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'p');
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'p' );
}
}
@@ -257,6 +264,13 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
public function getDescription() {
return 'Enumerate all pages sequentially in a given namespace';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'params', 'info' => 'Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator' ),
+ array( 'code' => 'params', 'info' => 'prlevel may not be used without prtype' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -272,6 +286,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryAllpages.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryAllpages.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index 1b1fe639..648da069 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -61,18 +61,18 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
)
);
- public function __construct($query, $moduleName) {
- extract($this->backlinksSettings[$moduleName]);
+ public function __construct( $query, $moduleName ) {
+ extract( $this->backlinksSettings[$moduleName] );
$this->resultArr = array();
- parent :: __construct($query, $moduleName, $code);
+ parent :: __construct( $query, $moduleName, $code );
$this->bl_ns = $prefix . '_namespace';
$this->bl_from = $prefix . '_from';
$this->bl_table = $linktbl;
$this->bl_code = $code;
$this->hasNS = $moduleName !== 'imageusage';
- if ($this->hasNS) {
+ if ( $this->hasNS ) {
$this->bl_title = $prefix . '_title';
$this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}";
$this->bl_fields = array (
@@ -96,210 +96,223 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function prepareFirstQuery($resultPageSet = null) {
+ 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'));
+ $this->addTables( array( $this->bl_table, 'page' ) );
+ $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());
- $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('page_namespace', $this->params['namespace']);
- if(!is_null($this->contID))
- $this->addWhere("{$this->bl_from}>={$this->contID}");
- if($this->params['filterredir'] == 'redirects')
- $this->addWhereFld('page_is_redirect', 1);
- if($this->params['filterredir'] == 'nonredirects')
- $this->addWhereFld('page_is_redirect', 0);
- $this->addOption('LIMIT', $this->params['limit'] + 1);
- $this->addOption('ORDER BY', $this->bl_from);
+ $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( 'page_namespace', $this->params['namespace'] );
+
+ if ( !is_null( $this->contID ) )
+ $this->addWhere( "{$this->bl_from}>={$this->contID}" );
+
+ if ( $this->params['filterredir'] == 'redirects' )
+ $this->addWhereFld( 'page_is_redirect', 1 );
+ else if ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect )
+ // bug 22245 - Check for !redirect, as filtering nonredirects, when getting what links to them is contradictory
+ $this->addWhereFld( 'page_is_redirect', 0 );
+
+ $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
+ $this->addOption( 'ORDER BY', $this->bl_from );
+ $this->addOption( 'STRAIGHT_JOIN' );
}
- private function prepareSecondQuery($resultPageSet = null) {
+ 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)
ORDER BY pl_namespace, pl_title, pl_from LIMIT 11
*/
$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'));
+ $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);
+ $this->addFields( $resultPageSet->getPageTableFields() );
+
+ $this->addFields( $this->bl_title );
+ if ( $this->hasNS )
+ $this->addFields( $this->bl_ns );
+
// We can't use LinkBatch here because $this->hasNS may be false
$titleWhere = array();
- foreach($this->redirTitles as $t)
- $titleWhere[] = "{$this->bl_title} = ".$db->addQuotes($t->getDBKey()).
- ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "");
- $this->addWhere($db->makeList($titleWhere, LIST_OR));
- $this->addWhereFld('page_namespace', $this->params['namespace']);
- if(!is_null($this->redirID))
+ foreach ( $this->redirTitles as $t )
+ $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $t->getDBkey() ) .
+ ( $this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "" );
+ $this->addWhere( $db->makeList( $titleWhere, LIST_OR ) );
+ $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
+
+ if ( !is_null( $this->redirID ) )
{
$first = $this->redirTitles[0];
- $title = $db->strencode($first->getDBKey());
+ $title = $db->strencode( $first->getDBkey() );
$ns = $first->getNamespace();
$from = $this->redirID;
- if($this->hasNS)
- $this->addWhere("{$this->bl_ns} > $ns OR ".
- "({$this->bl_ns} = $ns AND ".
- "({$this->bl_title} > '$title' OR ".
- "({$this->bl_title} = '$title' AND ".
- "{$this->bl_from} >= $from)))");
+ if ( $this->hasNS )
+ $this->addWhere( "{$this->bl_ns} > $ns OR " .
+ "({$this->bl_ns} = $ns AND " .
+ "({$this->bl_title} > '$title' OR " .
+ "({$this->bl_title} = '$title' AND " .
+ "{$this->bl_from} >= $from)))" );
else
- $this->addWhere("{$this->bl_title} > '$title' OR ".
- "({$this->bl_title} = '$title' AND ".
- "{$this->bl_from} >= $from)");
+ $this->addWhere( "{$this->bl_title} > '$title' OR " .
+ "({$this->bl_title} = '$title' AND " .
+ "{$this->bl_from} >= $from)" );
}
- if($this->params['filterredir'] == 'redirects')
- $this->addWhereFld('page_is_redirect', 1);
- if($this->params['filterredir'] == 'nonredirects')
- $this->addWhereFld('page_is_redirect', 0);
- $this->addOption('LIMIT', $this->params['limit'] + 1);
- $this->addOption('ORDER BY', $this->bl_sort);
- $this->addOption('USE INDEX', array('page' => 'PRIMARY'));
+ if ( $this->params['filterredir'] == 'redirects' )
+ $this->addWhereFld( 'page_is_redirect', 1 );
+ else 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 );
+ $this->addOption( 'USE INDEX', array( 'page' => 'PRIMARY' ) );
}
- 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' ) {
+ 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);
+ $this->prepareFirstQuery( $resultPageSet );
$db = $this->getDB();
- $res = $this->select(__METHOD__.'::firstQuery');
+ $res = $this->select( __METHOD__ . '::firstQuery' );
$count = 0;
$this->pageMap = array(); // Maps ns and title to pageid
$this->continueStr = null;
$this->redirTitles = array();
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $this->params['limit']) {
+ 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...
// Continue string preserved in case the redirect query doesn't pass the limit
- $this->continueStr = $this->getContinueStr($row->page_id);
+ $this->continueStr = $this->getContinueStr( $row->page_id );
break;
}
- if (is_null($resultPageSet))
- $this->extractRowInfo($row);
+ if ( is_null( $resultPageSet ) )
+ $this->extractRowInfo( $row );
else
{
$this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
- if($row->page_is_redirect)
- $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title);
- $resultPageSet->processDbRow($row);
+ if ( $row->page_is_redirect )
+ $this->redirTitles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
+
+ $resultPageSet->processDbRow( $row );
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if($this->redirect && count($this->redirTitles))
+ if ( $this->redirect && count( $this->redirTitles ) )
{
$this->resetQueryParams();
- $this->prepareSecondQuery($resultPageSet);
- $res = $this->select(__METHOD__.'::secondQuery');
+ $this->prepareSecondQuery( $resultPageSet );
+ $res = $this->select( __METHOD__ . '::secondQuery' );
$count = 0;
- while($row = $db->fetchObject($res))
+ while ( $row = $db->fetchObject( $res ) )
{
- if(++$count > $this->params['limit'])
+ 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)
- $parentID = $this->pageMap[$row->{$this->bl_ns}][$row->{$this->bl_title}];
+ if ( $this->hasNS )
+ $parentID = $this->pageMap[$row-> { $this->bl_ns } ][$row-> { $this->bl_title } ];
else
- $parentID = $this->pageMap[NS_IMAGE][$row->{$this->bl_title}];
- $this->continueStr = $this->getContinueRedirStr($parentID, $row->page_id);
+ $parentID = $this->pageMap[NS_IMAGE][$row-> { $this->bl_title } ];
+ $this->continueStr = $this->getContinueRedirStr( $parentID, $row->page_id );
break;
}
- if(is_null($resultPageSet))
- $this->extractRedirRowInfo($row);
+ if ( is_null( $resultPageSet ) )
+ $this->extractRedirRowInfo( $row );
else
- $resultPageSet->processDbRow($row);
+ $resultPageSet->processDbRow( $row );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
// Try to add the result data in one go and pray that it fits
- $fit = $this->getResult()->addValue('query', $this->getModuleName(), array_values($this->resultArr));
- if(!$fit)
+ $fit = $this->getResult()->addValue( 'query', $this->getModuleName(), array_values( $this->resultArr ) );
+ if ( !$fit )
{
// It didn't fit. Add elements one by one until the
// result is full.
- foreach($this->resultArr as $pageID => $arr)
+ foreach ( $this->resultArr as $pageID => $arr )
{
// Add the basic entry without redirlinks first
$fit = $this->getResult()->addValue(
- array('query', $this->getModuleName()),
- null, array_diff_key($arr, array('redirlinks' => '')));
- if(!$fit)
+ array( 'query', $this->getModuleName() ),
+ null, array_diff_key( $arr, array( 'redirlinks' => '' ) ) );
+ if ( !$fit )
{
- $this->continueStr = $this->getContinueStr($pageID);
+ $this->continueStr = $this->getContinueStr( $pageID );
break;
}
$hasRedirs = false;
- foreach((array)@$arr['redirlinks'] as $key => $redir)
+ foreach ( (array)@$arr['redirlinks'] as $key => $redir )
{
$fit = $this->getResult()->addValue(
- array('query', $this->getModuleName(), $pageID, 'redirlinks'),
- $key, $redir);
- if(!$fit)
+ array( 'query', $this->getModuleName(), $pageID, 'redirlinks' ),
+ $key, $redir );
+ if ( !$fit )
{
- $this->continueStr = $this->getContinueRedirStr($pageID, $redir['pageid']);
+ $this->continueStr = $this->getContinueRedirStr( $pageID, $redir['pageid'] );
break;
}
$hasRedirs = true;
}
- if($hasRedirs)
+ if ( $hasRedirs )
$this->getResult()->setIndexedTagName_internal(
- array('query', $this->getModuleName(), $pageID, 'redirlinks'),
- $this->bl_code);
- if(!$fit)
+ array( 'query', $this->getModuleName(), $pageID, 'redirlinks' ),
+ $this->bl_code );
+ if ( !$fit )
break;
}
- }
+ }
$this->getResult()->setIndexedTagName_internal(
- array('query', $this->getModuleName()),
- $this->bl_code);
+ array( 'query', $this->getModuleName() ),
+ $this->bl_code );
}
- if(!is_null($this->continueStr))
- $this->setContinueEnumParameter('continue', $this->continueStr);
+ if ( !is_null( $this->continueStr ) )
+ $this->setContinueEnumParameter( 'continue', $this->continueStr );
}
- private function extractRowInfo($row) {
+ private function extractRowInfo( $row ) {
$this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id;
- $t = Title::makeTitle($row->page_namespace, $row->page_title);
- $a = array('pageid' => intval($row->page_id));
- ApiQueryBase::addTitleInfo($a, $t);
- if($row->page_is_redirect)
+ $t = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $a = array( 'pageid' => intval( $row->page_id ) );
+ ApiQueryBase::addTitleInfo( $a, $t );
+ if ( $row->page_is_redirect )
{
$a['redirect'] = '';
$this->redirTitles[] = $t;
@@ -308,42 +321,42 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
$this->resultArr[$a['pageid']] = $a;
}
- private function extractRedirRowInfo($row)
+ private function extractRedirRowInfo( $row )
{
- $a['pageid'] = intval($row->page_id);
- ApiQueryBase::addTitleInfo($a, Title::makeTitle($row->page_namespace, $row->page_title));
- if($row->page_is_redirect)
+ $a['pageid'] = intval( $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_FILE;
- $parentID = $this->pageMap[$ns][$row->{$this->bl_title}];
+ $ns = $this->hasNS ? $row-> { $this->bl_ns } : NS_FILE;
+ $parentID = $this->pageMap[$ns][$row-> { $this->bl_title } ];
// Put all the results in an array first
$this->resultArr[$parentID]['redirlinks'][] = $a;
- $this->getResult()->setIndexedTagName($this->resultArr[$parentID]['redirlinks'], $this->bl_code);
+ $this->getResult()->setIndexedTagName( $this->resultArr[$parentID]['redirlinks'], $this->bl_code );
}
protected function processContinue() {
- if (!is_null($this->params['continue']))
+ if ( !is_null( $this->params['continue'] ) )
$this->parseContinueParam();
else {
if ( $this->params['title'] !== "" ) {
$title = Title::newFromText( $this->params['title'] );
if ( !$title ) {
- $this->dieUsageMsg(array('invalidtitle', $this->params['title']));
+ $this->dieUsageMsg( array( 'invalidtitle', $this->params['title'] ) );
} else {
$this->rootTitle = $title;
}
} else {
- $this->dieUsageMsg(array('missingparam', 'title'));
+ $this->dieUsageMsg( array( 'missingparam', 'title' ) );
}
}
// only image titles are allowed for the root in imageinfo mode
- if (!$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE)
- $this->dieUsage("The title for {$this->getModuleName()} query must be an image", 'bad_image_title');
+ if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE )
+ $this->dieUsage( "The title for {$this->getModuleName()} query must be an image", 'bad_image_title' );
}
protected function parseContinueParam() {
- $continueList = explode('|', $this->params['continue']);
+ $continueList = explode( '|', $this->params['continue'] );
// expected format:
// ns | key | id1 [| id2]
// ns+key: root title
@@ -352,33 +365,36 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
// 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')
+ $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->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')
+ $redirID = intval( @$continueList[3] );
+
+ if ( $redirID === 0 && @$continueList[3] !== '0' )
// This one isn't required
return;
$this->redirID = $redirID;
}
- protected function getContinueStr($lastPageID) {
+ protected function getContinueStr( $lastPageID ) {
return $this->rootTitle->getNamespace() .
'|' . $this->rootTitle->getDBkey() .
'|' . $lastPageID;
}
- protected function getContinueRedirStr($lastPageID, $lastRedirID) {
- return $this->getContinueStr($lastPageID) . '|' . $lastRedirID;
+ protected function getContinueRedirStr( $lastPageID, $lastRedirID ) {
+ return $this->getContinueStr( $lastPageID ) . '|' . $lastRedirID;
}
public function getAllowedParams() {
@@ -405,7 +421,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
)
);
- if($this->getModuleName() == 'embeddedin')
+ if ( $this->getModuleName() == 'embeddedin' )
return $retval;
$retval['redirect'] = false;
return $retval;
@@ -413,23 +429,24 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
public function getParamDescription() {
$retval = array (
- 'title' => 'Title to search. If null, titles= parameter will be used instead, but will be obsolete soon.',
+ 'title' => 'Title to search.',
'continue' => 'When more results are available, use this to continue.',
'namespace' => 'The namespace to enumerate.',
- 'filterredir' => 'How to filter for redirects'
);
- if($this->getModuleName() != 'embeddedin')
- return array_merge($retval, array(
+ 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."
- ));
+ 'filterredir' => "How to filter for redirects. If set to nonredirects when {$this->bl_code}redirect is enabled, this is only applied to the second level",
+ 'limit' => "How many total pages to return. If {$this->bl_code}redirect is enabled, limit applies to each level separately (which means you may get up to 2 * limit results)."
+ ) );
+ return array_merge( $retval, array(
+ 'filterredir' => 'How to filter for redirects',
+ 'limit' => 'How many total pages to return.'
+ ) );
}
public function getDescription() {
- switch ($this->getModuleName()) {
+ switch ( $this->getModuleName() ) {
case 'backlinks' :
return 'Find all pages that link to the given page';
case 'embeddedin' :
@@ -437,9 +454,18 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
case 'imageusage' :
return 'Find all pages that use the given image title.';
default :
- ApiBase :: dieDebug(__METHOD__, 'Unknown module name');
+ ApiBase :: dieDebug( __METHOD__, 'Unknown module name' );
}
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'invalidtitle', 'title' ),
+ array( 'missingparam', 'title' ),
+ array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ),
+ array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
static $examples = array (
@@ -461,6 +487,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBacklinks.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryBacklinks.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index 7e2b1d5e..893da566 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -39,8 +39,8 @@ abstract class ApiQueryBase extends ApiBase {
private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds;
- public function __construct($query, $moduleName, $paramPrefix = '') {
- parent :: __construct($query->getMain(), $moduleName, $paramPrefix);
+ public function __construct( $query, $moduleName, $paramPrefix = '' ) {
+ parent :: __construct( $query->getMain(), $moduleName, $paramPrefix );
$this->mQueryModule = $query;
$this->mDb = null;
$this->resetQueryParams();
@@ -74,14 +74,14 @@ abstract class ApiQueryBase extends ApiBase {
* @param $alias mixed 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))
- ApiBase :: dieDebug(__METHOD__, 'Multiple table aliases not supported');
- $this->tables = array_merge($this->tables, $tables);
+ protected function addTables( $tables, $alias = null ) {
+ if ( is_array( $tables ) ) {
+ if ( !is_null( $alias ) )
+ ApiBase :: dieDebug( __METHOD__, 'Multiple table aliases not supported' );
+ $this->tables = array_merge( $this->tables, $tables );
} else {
- if (!is_null($alias))
- $tables = $this->getAliasedName($tables, $alias);
+ if ( !is_null( $alias ) )
+ $tables = $this->getAliasedName( $tables, $alias );
$this->tables[] = $tables;
}
}
@@ -92,8 +92,8 @@ abstract class ApiQueryBase extends ApiBase {
* @param $alias string Alias
* @return string SQL
*/
- protected function getAliasedName($table, $alias) {
- return $this->getDB()->tableName($table) . ' ' . $alias;
+ protected function getAliasedName( $table, $alias ) {
+ return $this->getDB()->tableName( $table ) . ' ' . $alias;
}
/**
@@ -105,19 +105,19 @@ abstract class ApiQueryBase extends ApiBase {
* addWhere()-style array
* @param $join_conds array 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);
+ 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 $value mixed Field name or array of field names
*/
- protected function addFields($value) {
- if (is_array($value))
- $this->fields = array_merge($this->fields, $value);
+ protected function addFields( $value ) {
+ if ( is_array( $value ) )
+ $this->fields = array_merge( $this->fields, $value );
else
$this->fields[] = $value;
}
@@ -128,9 +128,9 @@ abstract class ApiQueryBase extends ApiBase {
* @param $condition bool If false, do nothing
* @return bool $condition
*/
- protected function addFieldsIf($value, $condition) {
- if ($condition) {
- $this->addFields($value);
+ protected function addFieldsIf( $value, $condition ) {
+ if ( $condition ) {
+ $this->addFields( $value );
return true;
}
return false;
@@ -147,12 +147,12 @@ abstract class ApiQueryBase extends ApiBase {
* to "foo=bar AND baz='3' AND bla='foo'"
* @param $value mixed String or array
*/
- protected function addWhere($value) {
- if (is_array($value)) {
+ protected function addWhere( $value ) {
+ if ( is_array( $value ) ) {
// Sanity check: don't insert empty arrays,
// Database::makeList() chokes on them
if ( count( $value ) )
- $this->where = array_merge($this->where, $value);
+ $this->where = array_merge( $this->where, $value );
}
else
$this->where[] = $value;
@@ -164,9 +164,9 @@ abstract class ApiQueryBase extends ApiBase {
* @param $condition boolIf false, do nothing
* @return bool $condition
*/
- protected function addWhereIf($value, $condition) {
- if ($condition) {
- $this->addWhere($value);
+ protected function addWhereIf( $value, $condition ) {
+ if ( $condition ) {
+ $this->addWhere( $value );
return true;
}
return false;
@@ -177,7 +177,7 @@ abstract class ApiQueryBase extends ApiBase {
* @param $field string Field name
* @param $value string Value; ignored if null or empty array;
*/
- protected function addWhereFld($field, $value) {
+ protected function addWhereFld( $field, $value ) {
// Use count() to its full documented capabilities to simultaneously
// test for null, empty array or empty countable object
if ( count( $value ) )
@@ -196,24 +196,24 @@ abstract class ApiQueryBase extends ApiBase {
* is the upper boundary, otherwise it's the lower boundary
* @param $sort bool If false, don't add an ORDER BY clause
*/
- protected function addWhereRange($field, $dir, $start, $end, $sort = true) {
- $isDirNewer = ($dir === 'newer');
- $after = ($isDirNewer ? '>=' : '<=');
- $before = ($isDirNewer ? '<=' : '>=');
+ protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) {
+ $isDirNewer = ( $dir === 'newer' );
+ $after = ( $isDirNewer ? '>=' : '<=' );
+ $before = ( $isDirNewer ? '<=' : '>=' );
$db = $this->getDB();
- if (!is_null($start))
- $this->addWhere($field . $after . $db->addQuotes($start));
+ if ( !is_null( $start ) )
+ $this->addWhere( $field . $after . $db->addQuotes( $start ) );
- if (!is_null($end))
- $this->addWhere($field . $before . $db->addQuotes($end));
+ if ( !is_null( $end ) )
+ $this->addWhere( $field . $before . $db->addQuotes( $end ) );
- if ($sort) {
- $order = $field . ($isDirNewer ? '' : ' DESC');
- if (!isset($this->options['ORDER BY']))
- $this->addOption('ORDER BY', $order);
+ if ( $sort ) {
+ $order = $field . ( $isDirNewer ? '' : ' DESC' );
+ if ( !isset( $this->options['ORDER BY'] ) )
+ $this->addOption( 'ORDER BY', $order );
else
- $this->addOption('ORDER BY', $this->options['ORDER BY'] . ', ' . $order);
+ $this->addOption( 'ORDER BY', $this->options['ORDER BY'] . ', ' . $order );
}
}
@@ -223,8 +223,8 @@ abstract class ApiQueryBase extends ApiBase {
* @param $name string Option name
* @param $value string Option value
*/
- protected function addOption($name, $value = null) {
- if (is_null($value))
+ protected function addOption( $name, $value = null ) {
+ if ( is_null( $value ) )
$this->options[] = $name;
else
$this->options[$name] = $value;
@@ -236,13 +236,12 @@ abstract class ApiQueryBase extends ApiBase {
* You should usually use __METHOD__ here
* @return ResultWrapper
*/
- protected function select($method) {
-
+ 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, $this->join_conds);
+ $res = $db->select( $this->tables, $this->fields, $this->where, $method, $this->options, $this->join_conds );
$this->profileDBOut();
return $res;
@@ -256,11 +255,11 @@ abstract class ApiQueryBase extends ApiBase {
protected function checkRowCount() {
$db = $this->getDB();
$this->profileDBIn();
- $rowcount = $db->estimateRowCount($this->tables, $this->fields, $this->where, __METHOD__, $this->options);
+ $rowcount = $db->estimateRowCount( $this->tables, $this->fields, $this->where, __METHOD__, $this->options );
$this->profileDBOut();
global $wgAPIMaxDBRows;
- if($rowcount > $wgAPIMaxDBRows)
+ if ( $rowcount > $wgAPIMaxDBRows )
return false;
return true;
}
@@ -272,8 +271,8 @@ abstract class ApiQueryBase extends ApiBase {
* @param $title Title
* @param $prefix string Module prefix
*/
- public static function addTitleInfo(&$arr, $title, $prefix='') {
- $arr[$prefix . 'ns'] = intval($title->getNamespace());
+ public static function addTitleInfo( &$arr, $title, $prefix = '' ) {
+ $arr[$prefix . 'ns'] = intval( $title->getNamespace() );
$arr[$prefix . 'title'] = $title->getPrefixedText();
}
@@ -282,7 +281,7 @@ abstract class ApiQueryBase extends ApiBase {
* using $pageSet->requestField('fieldName')
* @param $pageSet ApiPageSet
*/
- public function requestExtraData($pageSet) {
+ public function requestExtraData( $pageSet ) {
}
/**
@@ -299,12 +298,12 @@ abstract class ApiQueryBase extends ApiBase {
* @param $data array Data array à la ApiResult
* @return bool Whether the element fit in the result
*/
- protected function addPageSubItems($pageId, $data) {
+ protected function addPageSubItems( $pageId, $data ) {
$result = $this->getResult();
- $result->setIndexedTagName($data, $this->getModulePrefix());
- return $result->addValue(array('query', 'pages', intval($pageId)),
+ $result->setIndexedTagName( $data, $this->getModulePrefix() );
+ return $result->addValue( array( 'query', 'pages', intval( $pageId ) ),
$this->getModuleName(),
- $data);
+ $data );
}
/**
@@ -315,16 +314,16 @@ abstract class ApiQueryBase extends ApiBase {
* is used
* @return bool Whether the element fit in the result
*/
- protected function addPageSubItem($pageId, $item, $elemname = null) {
- if(is_null($elemname))
+ protected function addPageSubItem( $pageId, $item, $elemname = null ) {
+ if ( is_null( $elemname ) )
$elemname = $this->getModulePrefix();
$result = $this->getResult();
- $fit = $result->addValue(array('query', 'pages', $pageId,
- $this->getModuleName()), null, $item);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', 'pages', $pageId,
+ $this->getModuleName() ), null, $item );
+ if ( !$fit )
return false;
- $result->setIndexedTagName_internal(array('query', 'pages', $pageId,
- $this->getModuleName()), $elemname);
+ $result->setIndexedTagName_internal( array( 'query', 'pages', $pageId,
+ $this->getModuleName() ), $elemname );
return true;
}
@@ -333,11 +332,11 @@ abstract class ApiQueryBase extends ApiBase {
* @param $paramName string Parameter name
* @param $paramValue string Parameter value
*/
- protected function setContinueEnumParameter($paramName, $paramValue) {
- $paramName = $this->encodeParamName($paramName);
+ protected function setContinueEnumParameter( $paramName, $paramValue ) {
+ $paramName = $this->encodeParamName( $paramName );
$msg = array( $paramName => $paramValue );
$this->getResult()->disableSizeCheck();
- $this->getResult()->addValue('query-continue', $this->getModuleName(), $msg);
+ $this->getResult()->addValue( 'query-continue', $this->getModuleName(), $msg );
$this->getResult()->enableSizeCheck();
}
@@ -346,7 +345,7 @@ abstract class ApiQueryBase extends ApiBase {
* @return Database
*/
protected function getDB() {
- if (is_null($this->mDb))
+ if ( is_null( $this->mDb ) )
$this->mDb = $this->getQuery()->getDB();
return $this->mDb;
}
@@ -359,8 +358,8 @@ abstract class ApiQueryBase extends ApiBase {
* @param $groups array Query groups
* @return Database
*/
- public function selectNamedDB($name, $db, $groups) {
- $this->mDb = $this->getQuery()->getNamedDB($name, $db, $groups);
+ public function selectNamedDB( $name, $db, $groups ) {
+ $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups );
}
/**
@@ -376,13 +375,13 @@ abstract class ApiQueryBase extends ApiBase {
* @param $title string Page title with spaces
* @return string Page title with underscores
*/
- public function titleToKey($title) {
- # Don't throw an error if we got an empty string
- if(trim($title) == '')
+ public function titleToKey( $title ) {
+ // Don't throw an error if we got an empty string
+ if ( trim( $title ) == '' )
return '';
- $t = Title::newFromText($title);
- if(!$t)
- $this->dieUsageMsg(array('invalidtitle', $title));
+ $t = Title::newFromText( $title );
+ if ( !$t )
+ $this->dieUsageMsg( array( 'invalidtitle', $title ) );
return $t->getPrefixedDbKey();
}
@@ -391,14 +390,14 @@ abstract class ApiQueryBase extends ApiBase {
* @param $key string Page title with underscores
* @return string Page title with spaces
*/
- public function keyToTitle($key) {
- # Don't throw an error if we got an empty string
- if(trim($key) == '')
+ public function keyToTitle( $key ) {
+ // Don't throw an error if we got an empty string
+ if ( trim( $key ) == '' )
return '';
- $t = Title::newFromDbKey($key);
- # This really shouldn't happen but we gotta check anyway
- if(!$t)
- $this->dieUsageMsg(array('invalidtitle', $key));
+ $t = Title::newFromDbKey( $key );
+ // This really shouldn't happen but we gotta check anyway
+ if ( !$t )
+ $this->dieUsageMsg( array( 'invalidtitle', $key ) );
return $t->getPrefixedText();
}
@@ -407,8 +406,8 @@ abstract class ApiQueryBase extends ApiBase {
* @param $titlePart string Title part with spaces
* @return string Title part with underscores
*/
- public function titlePartToKey($titlePart) {
- return substr($this->titleToKey($titlePart . 'x'), 0, -1);
+ public function titlePartToKey( $titlePart ) {
+ return substr( $this->titleToKey( $titlePart . 'x' ), 0, - 1 );
}
/**
@@ -416,8 +415,15 @@ abstract class ApiQueryBase extends ApiBase {
* @param $keyPart string Key part with spaces
* @return string Key part with underscores
*/
- public function keyPartToTitle($keyPart) {
- return substr($this->keyToTitle($keyPart . 'x'), 0, -1);
+ public function keyPartToTitle( $keyPart ) {
+ return substr( $this->keyToTitle( $keyPart . 'x' ), 0, - 1 );
+ }
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'invalidtitle', 'title' ),
+ array( 'invalidtitle', 'key' ),
+ ) );
}
/**
@@ -425,7 +431,7 @@ abstract class ApiQueryBase extends ApiBase {
* @return string
*/
public static function getBaseVersion() {
- return __CLASS__ . ': $Id: ApiQueryBase.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryBase.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
@@ -436,8 +442,8 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
private $mIsGenerator;
- public function __construct($query, $moduleName, $paramPrefix = '') {
- parent :: __construct($query, $moduleName, $paramPrefix);
+ public function __construct( $query, $moduleName, $paramPrefix = '' ) {
+ parent :: __construct( $query, $moduleName, $paramPrefix );
$this->mIsGenerator = false;
}
@@ -454,11 +460,11 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
* @param $paramNames string Parameter name
* @return string Prefixed parameter name
*/
- public function encodeParamName($paramName) {
- if ($this->mIsGenerator)
- return 'g' . parent :: encodeParamName($paramName);
+ public function encodeParamName( $paramName ) {
+ if ( $this->mIsGenerator )
+ return 'g' . parent :: encodeParamName( $paramName );
else
- return parent :: encodeParamName($paramName);
+ return parent :: encodeParamName( $paramName );
}
/**
@@ -466,5 +472,5 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase {
* @param $resultPageSet ApiPageSet: All output should be appended to
* this object
*/
- public abstract function executeGenerator($resultPageSet);
+ public abstract function executeGenerator( $resultPageSet );
}
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index 64790037..8b321044 100644
--- a/includes/api/ApiQueryBlocks.php
+++ b/includes/api/ApiQueryBlocks.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -37,157 +37,163 @@ class ApiQueryBlocks extends ApiQueryBase {
var $users;
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'bk');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'bk' );
}
public function execute() {
global $wgUser;
$params = $this->extractRequestParams();
- if(isset($params['users']) && isset($params['ip']))
- $this->dieUsage('bkusers and bkip cannot be used together', 'usersandip');
+ 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']);
- $fld_by = isset($prop['by']);
- $fld_timestamp = isset($prop['timestamp']);
- $fld_expiry = isset($prop['expiry']);
- $fld_reason = isset($prop['reason']);
- $fld_range = isset($prop['range']);
- $fld_flags = isset($prop['flags']);
+ $prop = array_flip( $params['prop'] );
+ $fld_id = isset( $prop['id'] );
+ $fld_user = isset( $prop['user'] );
+ $fld_by = isset( $prop['by'] );
+ $fld_timestamp = isset( $prop['timestamp'] );
+ $fld_expiry = isset( $prop['expiry'] );
+ $fld_reason = isset( $prop['reason'] );
+ $fld_range = isset( $prop['range'] );
+ $fld_flags = isset( $prop['flags'] );
$result = $this->getResult();
$pageSet = $this->getPageSet();
$titles = $pageSet->getTitles();
$data = array();
- $this->addTables('ipblocks');
- if($fld_id)
- $this->addFields('ipb_id');
- if($fld_user)
- $this->addFields(array('ipb_address', 'ipb_user', 'ipb_auto'));
- if($fld_by)
+ $this->addTables( 'ipblocks' );
+ $this->addFields( 'ipb_auto' );
+
+ if ( $fld_id )
+ $this->addFields( 'ipb_id' );
+ if ( $fld_user )
+ $this->addFields( array( 'ipb_address', 'ipb_user' ) );
+ if ( $fld_by )
{
- $this->addTables('user');
- $this->addFields(array('ipb_by', 'user_name'));
- $this->addWhere('user_id = ipb_by');
+ $this->addTables( 'user' );
+ $this->addFields( array( 'ipb_by', 'user_name' ) );
+ $this->addWhere( 'user_id = ipb_by' );
}
- if($fld_timestamp)
- $this->addFields('ipb_timestamp');
- if($fld_expiry)
- $this->addFields('ipb_expiry');
- if($fld_reason)
- $this->addFields('ipb_reason');
- if($fld_range)
- $this->addFields(array('ipb_range_start', 'ipb_range_end'));
- if($fld_flags)
- $this->addFields(array('ipb_auto', 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk'));
+ if ( $fld_timestamp )
+ $this->addFields( 'ipb_timestamp' );
+ if ( $fld_expiry )
+ $this->addFields( 'ipb_expiry' );
+ if ( $fld_reason )
+ $this->addFields( 'ipb_reason' );
+ if ( $fld_range )
+ $this->addFields( array( 'ipb_range_start', 'ipb_range_end' ) );
+ if ( $fld_flags )
+ $this->addFields( array( 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk' ) );
- $this->addOption('LIMIT', $params['limit'] + 1);
- $this->addWhereRange('ipb_timestamp', $params['dir'], $params['start'], $params['end']);
- if(isset($params['ids']))
- $this->addWhereFld('ipb_id', $params['ids']);
- if(isset($params['users']))
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $this->addWhereRange( 'ipb_timestamp', $params['dir'], $params['start'], $params['end'] );
+ if ( isset( $params['ids'] ) )
+ $this->addWhereFld( 'ipb_id', $params['ids'] );
+ if ( isset( $params['users'] ) )
{
- foreach((array)$params['users'] as $u)
- $this->prepareUsername($u);
- $this->addWhereFld('ipb_address', $this->usernames);
+ foreach ( (array)$params['users'] as $u )
+ $this->prepareUsername( $u );
+ $this->addWhereFld( 'ipb_address', $this->usernames );
+ $this->addWhereFld( 'ipb_auto', 0 );
}
- if(isset($params['ip']))
+ if ( isset( $params['ip'] ) )
{
- list($ip, $range) = IP::parseCIDR($params['ip']);
- if($ip && $range)
+ 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);
+ // 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%'",
+ $lower = $upper = IP::toHex( $params['ip'] );
+ $prefix = substr( $lower, 0, 4 );
+
+ $db = $this->getDB();
+ $this->addWhere( array(
+ 'ipb_range_start' . $db->buildLike( $prefix, $db->anyString() ),
"ipb_range_start <= '$lower'",
- "ipb_range_end >= '$upper'"
- ));
+ "ipb_range_end >= '$upper'",
+ 'ipb_auto' => 0
+ ) );
}
- if(!$wgUser->isAllowed('hideuser'))
- $this->addWhereFld('ipb_deleted', 0);
+ if ( !$wgUser->isAllowed( 'hideuser' ) )
+ $this->addWhereFld( 'ipb_deleted', 0 );
// Purge expired entries on one in every 10 queries
- if(!mt_rand(0, 10))
+ if ( !mt_rand( 0, 10 ) )
Block::purgeExpired();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$count = 0;
- while($row = $res->fetchObject())
+ while ( $row = $res->fetchObject() )
{
- if(++$count > $params['limit'])
+ if ( ++$count > $params['limit'] )
{
// We've had enough
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ) );
break;
}
$block = array();
- if($fld_id)
+ if ( $fld_id )
$block['id'] = $row->ipb_id;
- if($fld_user && !$row->ipb_auto)
+ if ( $fld_user && !$row->ipb_auto )
$block['user'] = $row->ipb_address;
- if($fld_by)
+ if ( $fld_by )
$block['by'] = $row->user_name;
- if($fld_timestamp)
- $block['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ipb_timestamp);
- if($fld_expiry)
- $block['expiry'] = Block::decodeExpiry($row->ipb_expiry, TS_ISO_8601);
- if($fld_reason)
+ if ( $fld_timestamp )
+ $block['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp );
+ if ( $fld_expiry )
+ $block['expiry'] = Block::decodeExpiry( $row->ipb_expiry, TS_ISO_8601 );
+ if ( $fld_reason )
$block['reason'] = $row->ipb_reason;
- if($fld_range)
+ if ( $fld_range && !$row->ipb_auto )
{
- $block['rangestart'] = IP::hexToQuad($row->ipb_range_start);
- $block['rangeend'] = IP::hexToQuad($row->ipb_range_end);
+ $block['rangestart'] = IP::hexToQuad( $row->ipb_range_start );
+ $block['rangeend'] = IP::hexToQuad( $row->ipb_range_end );
}
- if($fld_flags)
+ if ( $fld_flags )
{
// For clarity, these flags use the same names as their action=block counterparts
- if($row->ipb_auto)
+ if ( $row->ipb_auto )
$block['automatic'] = '';
- if($row->ipb_anon_only)
+ if ( $row->ipb_anon_only )
$block['anononly'] = '';
- if($row->ipb_create_account)
+ if ( $row->ipb_create_account )
$block['nocreate'] = '';
- if($row->ipb_enable_autoblock)
+ if ( $row->ipb_enable_autoblock )
$block['autoblock'] = '';
- if($row->ipb_block_email)
+ if ( $row->ipb_block_email )
$block['noemail'] = '';
- if($row->ipb_deleted)
+ if ( $row->ipb_deleted )
$block['hidden'] = '';
- if($row->ipb_allow_usertalk)
+ if ( $row->ipb_allow_usertalk )
$block['allowusertalk'] = '';
}
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $block);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $block );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ) );
break;
}
}
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'block');
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'block' );
}
- protected function prepareUsername($user)
+ protected function prepareUsername( $user )
{
- if(!$user)
- $this->dieUsage('User parameter may not be empty', 'param_user');
- $name = User::isIP($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');
+ : User::getCanonicalName( $user, 'valid' );
+ if ( $name === false )
+ $this->dieUsage( "User name {$user} is not valid", 'param_user' );
$this->usernames[] = $name;
}
@@ -246,7 +252,7 @@ class ApiQueryBlocks extends ApiQueryBase {
'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.'),
+ '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',
);
@@ -255,6 +261,15 @@ class ApiQueryBlocks extends ApiQueryBase {
public function getDescription() {
return 'List all blocked users and IP addresses.';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'usersandip', 'info' => 'bkusers and bkip cannot be used together' ),
+ array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ),
+ array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ),
+ array( 'code' => 'param_user', 'info' => 'User name user is not valid' ),
+ ) );
+ }
protected function getExamples() {
return array ( 'api.php?action=query&list=blocks',
@@ -263,6 +278,6 @@ class ApiQueryBlocks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryBlocks.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryBlocks.php 69578 2010-07-20 02:46:20Z tstarling $';
}
} \ No newline at end of file
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 15e1ce13..03135052 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryCategories extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'cl');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'cl' );
}
public function execute() {
@@ -47,144 +47,134 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
- if ($this->getPageSet()->getGoodTitleCount() == 0)
+ if ( $this->getPageSet()->getGoodTitleCount() == 0 )
return; // nothing to do
$params = $this->extractRequestParams();
- $prop = $params['prop'];
- $show = array_flip((array)$params['show']);
+ $prop = array_flip( (array)$params['prop'] );
+ $show = array_flip( (array)$params['show'] );
- $this->addFields(array (
+ $this->addFields( array (
'cl_from',
'cl_to'
- ));
-
- $fld_sortkey = $fld_timestamp = false;
- if (!is_null($prop)) {
- foreach($prop as $p) {
- switch ($p) {
- case 'sortkey':
- $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->addFieldsIf( 'cl_sortkey', isset( $prop['sortkey'] ) );
+ $this->addFieldsIf( 'cl_timestamp', isset( $prop['timestamp'] ) );
- $this->addTables('categorylinks');
- $this->addWhereFld('cl_from', array_keys($this->getPageSet()->getGoodTitles()));
- if(!is_null($params['categories']))
+ $this->addTables( 'categorylinks' );
+ $this->addWhereFld( 'cl_from', array_keys( $this->getPageSet()->getGoodTitles() ) );
+ if ( !is_null( $params['categories'] ) )
{
$cats = array();
- foreach($params['categories'] as $cat)
+ foreach ( $params['categories'] as $cat )
{
- $title = Title::newFromText($cat);
- if(!$title || $title->getNamespace() != NS_CATEGORY)
- $this->setWarning("``$cat'' is not a category");
+ $title = Title::newFromText( $cat );
+ if ( !$title || $title->getNamespace() != NS_CATEGORY )
+ $this->setWarning( "``$cat'' is not a category" );
else
$cats[] = $title->getDBkey();
}
- $this->addWhereFld('cl_to', $cats);
+ $this->addWhereFld( 'cl_to', $cats );
}
- 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')");
+
+ 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')" );
}
- if(isset($show['hidden']) && isset($show['!hidden']))
- $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
- if(isset($show['hidden']) || isset($show['!hidden']))
+
+ if ( isset( $show['hidden'] ) && isset( $show['!hidden'] ) )
+ $this->dieUsageMsg( array( 'show' ) );
+ if ( isset( $show['hidden'] ) || isset( $show['!hidden'] ) || isset( $prop['hidden'] ) )
{
- $this->addOption('STRAIGHT_JOIN');
- $this->addTables(array('page', 'page_props'));
- $this->addJoinConds(array(
- 'page' => array('LEFT JOIN', array(
+ $this->addOption( 'STRAIGHT_JOIN' );
+ $this->addTables( array( 'page', 'page_props' ) );
+ $this->addFieldsIf( 'pp_propname', isset( $prop['hidden'] ) );
+ $this->addJoinConds( array(
+ 'page' => array( 'LEFT JOIN', array(
'page_namespace' => NS_CATEGORY,
- 'page_title = cl_to')),
- 'page_props' => array('LEFT JOIN', array(
+ 'page_title = cl_to' ) ),
+ 'page_props' => array( 'LEFT JOIN', array(
'pp_page=page_id',
- 'pp_propname' => 'hiddencat'))
- ));
- if(isset($show['hidden']))
- $this->addWhere(array('pp_propname IS NOT NULL'));
- else
- $this->addWhere(array('pp_propname IS NULL'));
+ 'pp_propname' => 'hiddencat' ) )
+ ) );
+ if ( isset( $show['hidden'] ) )
+ $this->addWhere( array( 'pp_propname IS NOT NULL' ) );
+ else if ( isset( $show['!hidden'] ) )
+ $this->addWhere( array( 'pp_propname IS NULL' ) );
}
- $this->addOption('USE INDEX', array('categorylinks' => 'cl_from'));
- # 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');
+ $this->addOption( 'USE INDEX', array( 'categorylinks' => 'cl_from' ) );
+ // 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");
+ $this->addOption( 'ORDER BY', "cl_from, cl_to" );
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if (++$count > $params['limit']) {
+ 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));
+ $this->setContinueEnumParameter( 'continue', $row->cl_from .
+ '|' . $this->keyToTitle( $row->cl_to ) );
break;
}
- $title = Title :: makeTitle(NS_CATEGORY, $row->cl_to);
+ $title = Title :: makeTitle( NS_CATEGORY, $row->cl_to );
$vals = array();
- ApiQueryBase :: addTitleInfo($vals, $title);
- if ($fld_sortkey)
+ ApiQueryBase :: addTitleInfo( $vals, $title );
+ if ( isset( $prop['sortkey'] ) )
$vals['sortkey'] = $row->cl_sortkey;
- if ($fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp);
+ if ( isset( $prop['timestamp'] ) )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->cl_timestamp );
+ if ( isset( $prop['hidden'] ) && !is_null( $row->pp_propname ) )
+ $vals['hidden'] = '';
- $fit = $this->addPageSubItem($row->cl_from, $vals);
- if(!$fit)
+ $fit = $this->addPageSubItem( $row->cl_from, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue', $row->cl_from .
- '|' . $this->keyToTitle($row->cl_to));
+ $this->setContinueEnumParameter( 'continue', $row->cl_from .
+ '|' . $this->keyToTitle( $row->cl_to ) );
break;
}
}
} else {
$titles = array();
- while ($row = $db->fetchObject($res)) {
- if (++$count > $params['limit']) {
+ 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));
+ $this->setContinueEnumParameter( 'continue', $row->cl_from .
+ '|' . $this->keyToTitle( $row->cl_to ) );
break;
}
- $titles[] = Title :: makeTitle(NS_CATEGORY, $row->cl_to);
+ $titles[] = Title :: makeTitle( NS_CATEGORY, $row->cl_to );
}
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
public function getAllowedParams() {
@@ -194,6 +184,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
ApiBase :: PARAM_TYPE => array (
'sortkey',
'timestamp',
+ 'hidden',
)
),
'show' => array(
@@ -230,6 +221,12 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
public function getDescription() {
return 'List all categories the page(s) belong to';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'show' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -241,6 +238,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategories.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryCategories.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php
index f3d45ccf..4df2f181 100644
--- a/includes/api/ApiQueryCategoryInfo.php
+++ b/includes/api/ApiQueryCategoryInfo.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryCategoryInfo extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'ci');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'ci' );
}
public function execute() {
@@ -50,51 +50,53 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
$titles = $this->getPageSet()->getGoodTitles() +
$this->getPageSet()->getMissingTitles();
$cattitles = array();
- foreach($categories as $c)
+ foreach ( $categories as $c )
{
$t = $titles[$c];
- $cattitles[$c] = $t->getDBKey();
+ $cattitles[$c] = $t->getDBkey();
}
- $this->addTables(array('category', 'page', 'page_props'));
- $this->addJoinConds(array(
- 'page' => array('LEFT JOIN', array(
+ $this->addTables( array( 'category', 'page', 'page_props' ) );
+ $this->addJoinConds( array(
+ 'page' => array( 'LEFT JOIN', array(
'page_namespace' => NS_CATEGORY,
- 'page_title=cat_title')),
- 'page_props' => array('LEFT JOIN', array(
+ 'page_title=cat_title' ) ),
+ 'page_props' => array( 'LEFT JOIN', array(
'pp_page=page_id',
- 'pp_propname' => 'hiddencat')),
- ));
- $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden'));
- $this->addWhere(array('cat_title' => $cattitles));
- if(!is_null($params['continue']))
+ 'pp_propname' => 'hiddencat' ) ),
+ ) );
+
+ $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden' ) );
+ $this->addWhere( array( 'cat_title' => $cattitles ) );
+
+ if ( !is_null( $params['continue'] ) )
{
- $title = $this->getDB()->addQuotes($params['continue']);
- $this->addWhere("cat_title >= $title");
- }
- $this->addOption('ORDER BY', 'cat_title');
+ $title = $this->getDB()->addQuotes( $params['continue'] );
+ $this->addWhere( "cat_title >= $title" );
+ }
+ $this->addOption( 'ORDER BY', 'cat_title' );
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
- $catids = array_flip($cattitles);
- while($row = $db->fetchObject($res))
+ $catids = array_flip( $cattitles );
+ while ( $row = $db->fetchObject( $res ) )
{
$vals = array();
- $vals['size'] = intval($row->cat_pages);
+ $vals['size'] = intval( $row->cat_pages );
$vals['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files;
- $vals['files'] = intval($row->cat_files);
- $vals['subcats'] = intval($row->cat_subcats);
- if($row->cat_hidden)
+ $vals['files'] = intval( $row->cat_files );
+ $vals['subcats'] = intval( $row->cat_subcats );
+ if ( $row->cat_hidden )
$vals['hidden'] = '';
- $fit = $this->addPageSubItems($catids[$row->cat_title], $vals);
- if(!$fit)
+ $fit = $this->addPageSubItems( $catids[$row->cat_title], $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue', $row->cat_title);
+ $this->setContinueEnumParameter( 'continue', $row->cat_title );
break;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
public function getCacheMode( $params ) {
@@ -122,6 +124,6 @@ class ApiQueryCategoryInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 45461abd..107f5049 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'cm');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'cm' );
}
public function execute() {
@@ -47,113 +47,128 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
- if ( !isset($params['title']) || is_null($params['title']) )
- $this->dieUsage("The cmtitle parameter is required", 'notitle');
- $categoryTitle = Title::newFromText($params['title']);
+ 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');
+ $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']);
- $fld_sortkey = isset($prop['sortkey']);
- $fld_timestamp = isset($prop['timestamp']);
+ $prop = array_flip( $params['prop'] );
+ $fld_ids = isset( $prop['ids'] );
+ $fld_title = isset( $prop['title'] );
+ $fld_sortkey = isset( $prop['sortkey'] );
+ $fld_timestamp = isset( $prop['timestamp'] );
- if (is_null($resultPageSet)) {
- $this->addFields(array('cl_from', 'cl_sortkey', 'page_namespace', 'page_title'));
- $this->addFieldsIf('page_id', $fld_ids);
+ if ( is_null( $resultPageSet ) ) {
+ $this->addFields( array( 'cl_from', 'cl_sortkey', 'page_namespace', 'page_title' ) );
+ $this->addFieldsIf( 'page_id', $fld_ids );
} else {
- $this->addFields($resultPageSet->getPageTableFields()); // will include page_ id, ns, title
- $this->addFields(array('cl_from', 'cl_sortkey'));
+ $this->addFields( $resultPageSet->getPageTableFields() ); // will include page_ id, ns, title
+ $this->addFields( array( 'cl_from', 'cl_sortkey' ) );
}
- $this->addFieldsIf('cl_timestamp', $fld_timestamp || $params['sort'] == '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');
+ if ( $params['sort'] == 'timestamp' )
+ $this->addOption( 'USE INDEX', 'cl_timestamp' );
else
- $this->addOption('USE INDEX', 'cl_sortkey');
-
- $this->addWhere('cl_from=page_id');
- $this->setContinuation($params['continue'], $params['dir']);
- $this->addWhereFld('cl_to', $categoryTitle->getDBkey());
- $this->addWhereFld('page_namespace', $params['namespace']);
- if($params['sort'] == 'timestamp')
- $this->addWhereRange('cl_timestamp', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['start'], $params['end']);
+ $this->addOption( 'USE INDEX', 'cl_sortkey' );
+
+ $this->addWhere( 'cl_from=page_id' );
+ $this->setContinuation( $params['continue'], $params['dir'] );
+ $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() );
+ // Scanning large datasets for rare categories sucks, and I already told
+ // how to have efficient subcategory access :-) ~~~~ (oh well, domas)
+ global $wgMiserMode;
+ $miser_ns = array();
+ if ( $wgMiserMode ) {
+ $miser_ns = $params['namespace'];
+ } else {
+ $this->addWhereFld( 'page_namespace', $params['namespace'] );
+ }
+ if ( $params['sort'] == 'timestamp' )
+ $this->addWhereRange( 'cl_timestamp', ( $params['dir'] == 'asc' ? 'newer' : 'older' ), $params['start'], $params['end'] );
else
{
- $this->addWhereRange('cl_sortkey', ($params['dir'] == 'asc' ? 'newer' : 'older'), $params['startsortkey'], $params['endsortkey']);
- $this->addWhereRange('cl_from', ($params['dir'] == 'asc' ? 'newer' : 'older'), null, null);
+ $this->addWhereRange( 'cl_sortkey', ( $params['dir'] == 'asc' ? 'newer' : 'older' ), $params['startsortkey'], $params['endsortkey'] );
+ $this->addWhereRange( 'cl_from', ( $params['dir'] == 'asc' ? 'newer' : 'older' ), null, null );
}
$limit = $params['limit'];
- $this->addOption('LIMIT', $limit +1);
+ $this->addOption( 'LIMIT', $limit + 1 );
$db = $this->getDB();
$data = array ();
$count = 0;
$lastSortKey = null;
- $res = $this->select(__METHOD__);
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ $res = $this->select( __METHOD__ );
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
// TODO: Security issue - if the user has no right to view next title, it will still be shown
- if ($params['sort'] == 'timestamp')
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->cl_timestamp));
+ if ( $params['sort'] == 'timestamp' )
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) );
else
- $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey));
+ $this->setContinueEnumParameter( 'continue', $this->getContinueStr( $row, $lastSortKey ) );
break;
}
- if (is_null($resultPageSet)) {
+ // Since domas won't tell anyone what he told long ago, apply
+ // cmnamespace here. This means the query may return 0 actual
+ // results, but on the other hand it could save returning 5000
+ // useless results to the client. ~~~~
+ if ( count( $miser_ns ) && !in_array( $row->page_namespace, $miser_ns ) )
+ continue;
+
+ if ( is_null( $resultPageSet ) ) {
$vals = array();
- if ($fld_ids)
- $vals['pageid'] = intval($row->page_id);
- if ($fld_title) {
- $title = Title :: makeTitle($row->page_namespace, $row->page_title);
- ApiQueryBase::addTitleInfo($vals, $title);
+ if ( $fld_ids )
+ $vals['pageid'] = intval( $row->page_id );
+ if ( $fld_title ) {
+ $title = Title :: makeTitle( $row->page_namespace, $row->page_title );
+ ApiQueryBase::addTitleInfo( $vals, $title );
}
- if ($fld_sortkey)
+ if ( $fld_sortkey )
$vals['sortkey'] = $row->cl_sortkey;
- if ($fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp);
- $fit = $this->getResult()->addValue(array('query', $this->getModuleName()),
- null, $vals);
- if(!$fit)
+ if ( $fld_timestamp )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->cl_timestamp );
+ $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ),
+ null, $vals );
+ if ( !$fit )
{
- if ($params['sort'] == 'timestamp')
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->cl_timestamp));
+ if ( $params['sort'] == 'timestamp' )
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) );
else
- $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey));
+ $this->setContinueEnumParameter( 'continue', $this->getContinueStr( $row, $lastSortKey ) );
break;
}
} else {
- $resultPageSet->processDbRow($row);
+ $resultPageSet->processDbRow( $row );
}
$lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$this->getResult()->setIndexedTagName_internal(
- array('query', $this->getModuleName()), 'cm');
+ array( 'query', $this->getModuleName() ), 'cm' );
}
}
- private function getContinueStr($row, $lastSortKey) {
+ private function getContinueStr( $row, $lastSortKey ) {
$ret = $row->cl_sortkey . '|';
- if ($row->cl_sortkey == $lastSortKey) // duplicate sort key, add cl_from
+ if ( $row->cl_sortkey == $lastSortKey ) // duplicate sort key, add cl_from
$ret .= $row->cl_from;
return $ret;
}
@@ -161,24 +176,24 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
/**
* Add DB WHERE clause to continue previous query based on 'continue' parameter
*/
- private function setContinuation($continue, $dir) {
- if (is_null($continue))
+ private function setContinuation( $continue, $dir ) {
+ if ( is_null( $continue ) )
return; // This is not a continuation request
- $pos = strrpos($continue, '|');
- $sortkey = substr($continue, 0, $pos);
- $fromstr = substr($continue, $pos + 1);
- $from = intval($fromstr);
+ $pos = strrpos( $continue, '|' );
+ $sortkey = substr( $continue, 0, $pos );
+ $fromstr = substr( $continue, $pos + 1 );
+ $from = intval( $fromstr );
- if ($from == 0 && strlen($fromstr) > 0)
- $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue");
+ if ( $from == 0 && strlen( $fromstr ) > 0 )
+ $this->dieUsage( "Invalid continue param. You should pass the original value returned by the previous query", "badcontinue" );
- $encSortKey = $this->getDB()->addQuotes($sortkey);
- $encFrom = $this->getDB()->addQuotes($from);
+ $encSortKey = $this->getDB()->addQuotes( $sortkey );
+ $encFrom = $this->getDB()->addQuotes( $from );
- $op = ($dir == 'desc' ? '<' : '>');
+ $op = ( $dir == 'desc' ? '<' : '>' );
- if ($from != 0) {
+ if ( $from != 0 ) {
// Duplicate sort key continue
$this->addWhere( "cl_sortkey$op$encSortKey OR (cl_sortkey=$encSortKey AND cl_from$op=$encFrom)" );
} else {
@@ -237,7 +252,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
}
public function getParamDescription() {
- return array (
+ global $wgMiserMode;
+ $desc = array (
'title' => 'Which category to enumerate (required). Must include Category: prefix',
'prop' => 'What pieces of information to include',
'namespace' => 'Only include pages in these namespaces',
@@ -250,11 +266,27 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
'continue' => 'For large categories, give the value retured from previous query',
'limit' => 'The maximum number of pages to return.',
);
+ if ( $wgMiserMode ) {
+ $desc['namespace'] = array(
+ $desc['namespace'],
+ 'NOTE: Due to $wgMiserMode, using this may result in fewer than "limit" results',
+ 'returned before continuing; in extreme cases, zero results may be returned.',
+ );
+ }
+ return $desc;
}
public function getDescription() {
return 'List all pages in a given category';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'notitle', 'info' => 'The cmtitle parameter is required' ),
+ array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ),
+ array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -266,6 +298,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index bd767b1b..b26c7051 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,27 +35,28 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryDeletedrevs extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'dr');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'dr' );
}
public function execute() {
global $wgUser;
// Before doing anything at all, let's check permissions
- if(!$wgUser->isAllowed('deletedhistory'))
- $this->dieUsage('You don\'t have permission to view deleted revision information', 'permissiondenied');
+ if ( !$wgUser->isAllowed( 'deletedhistory' ) )
+ $this->dieUsage( 'You don\'t have permission to view deleted revision information', 'permissiondenied' );
$db = $this->getDB();
- $params = $this->extractRequestParams(false);
- $prop = array_flip($params['prop']);
- $fld_revid = isset($prop['revid']);
- $fld_user = isset($prop['user']);
- $fld_comment = isset($prop['comment']);
- $fld_minor = isset($prop['minor']);
- $fld_len = isset($prop['len']);
- $fld_content = isset($prop['content']);
- $fld_token = isset($prop['token']);
+ $params = $this->extractRequestParams( false );
+ $prop = array_flip( $params['prop'] );
+ $fld_revid = isset( $prop['revid'] );
+ $fld_user = isset( $prop['user'] );
+ $fld_comment = isset( $prop['comment'] );
+ $fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $fld_minor = isset( $prop['minor'] );
+ $fld_len = isset( $prop['len'] );
+ $fld_content = isset( $prop['content'] );
+ $fld_token = isset( $prop['token'] );
$result = $this->getResult();
$pageSet = $this->getPageSet();
@@ -67,36 +68,35 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
// 'user': List deleted revs by a certain user
// 'all': List all deleted revs
$mode = 'all';
- if(count($titles) > 0)
+ if ( count( $titles ) > 0 )
$mode = 'revs';
- else if(!is_null($params['user']))
+ else if ( !is_null( $params['user'] ) )
$mode = 'user';
- if(!is_null($params['user']) && !is_null($params['excludeuser']))
- $this->dieUsage('user and excludeuser cannot be used together', 'badparams');
+ if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) )
+ $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
- $this->addTables('archive');
- $this->addWhere('ar_deleted = 0');
- $this->addFields(array('ar_title', 'ar_namespace', 'ar_timestamp'));
- if($fld_revid)
- $this->addFields('ar_rev_id');
- if($fld_user)
- $this->addFields('ar_user_text');
- if($fld_comment)
- $this->addFields('ar_comment');
- if($fld_minor)
- $this->addFields('ar_minor_edit');
- if($fld_len)
- $this->addFields('ar_len');
- if($fld_content)
- {
- $this->addTables('text');
- $this->addFields(array('ar_text', 'ar_text_id', 'old_text', 'old_flags'));
- $this->addWhere('ar_text_id = old_id');
+ $this->addTables( 'archive' );
+ $this->addWhere( 'ar_deleted = 0' );
+ $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp' ) );
+ if ( $fld_revid )
+ $this->addFields( 'ar_rev_id' );
+ if ( $fld_user )
+ $this->addFields( 'ar_user_text' );
+ if ( $fld_comment || $fld_parsedcomment )
+ $this->addFields( 'ar_comment' );
+ if ( $fld_minor )
+ $this->addFields( 'ar_minor_edit' );
+ if ( $fld_len )
+ $this->addFields( 'ar_len' );
+ if ( $fld_content ) {
+ $this->addTables( 'text' );
+ $this->addFields( array( 'ar_text', 'ar_text_id', 'old_text', 'old_flags' ) );
+ $this->addWhere( 'ar_text_id = old_id' );
// This also means stricter restrictions
- if(!$wgUser->isAllowed('undelete'))
- $this->dieUsage('You don\'t have permission to view deleted revision content', 'permissiondenied');
+ if ( !$wgUser->isAllowed( 'undelete' ) )
+ $this->dieUsage( 'You don\'t have permission to view deleted revision content', 'permissiondenied' );
}
// Check limits
$userMax = $fld_content ? ApiBase :: LIMIT_SML1 : ApiBase :: LIMIT_BIG1;
@@ -104,143 +104,136 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
$limit = $params['limit'];
- if( $limit == 'max' ) {
+ if ( $limit == 'max' ) {
$limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$this->getResult()->addValue( 'limits', $this->getModuleName(), $limit );
}
- $this->validateLimit('limit', $limit, 1, $userMax, $botMax);
+ $this->validateLimit( 'limit', $limit, 1, $userMax, $botMax );
- if($fld_token)
+ if ( $fld_token )
// Undelete tokens are identical for all pages, so we cache one here
$token = $wgUser->editToken();
// We need a custom WHERE clause that matches all titles.
- if($mode == 'revs')
- {
- $lb = new LinkBatch($titles);
- $where = $lb->constructSet('ar', $db);
- $this->addWhere($where);
- }
- elseif($mode == 'all')
- {
- $this->addWhereFld('ar_namespace', $params['namespace']);
- if(!is_null($params['from']))
+ if ( $mode == 'revs' ) {
+ $lb = new LinkBatch( $titles );
+ $where = $lb->constructSet( 'ar', $db );
+ $this->addWhere( $where );
+ } elseif ( $mode == 'all' ) {
+ $this->addWhereFld( 'ar_namespace', $params['namespace'] );
+ if ( !is_null( $params['from'] ) )
{
- $from = $this->getDB()->strencode($this->titleToKey($params['from']));
- $this->addWhere("ar_title >= '$from'");
+ $from = $this->getDB()->strencode( $this->titleToKey( $params['from'] ) );
+ $this->addWhere( "ar_title >= '$from'" );
}
}
- if(!is_null($params['user'])) {
- $this->addWhereFld('ar_user_text', $params['user']);
- } elseif(!is_null($params['excludeuser'])) {
- $this->addWhere('ar_user_text != ' .
- $this->getDB()->addQuotes($params['excludeuser']));
+ if ( !is_null( $params['user'] ) ) {
+ $this->addWhereFld( 'ar_user_text', $params['user'] );
+ } elseif ( !is_null( $params['excludeuser'] ) ) {
+ $this->addWhere( 'ar_user_text != ' .
+ $this->getDB()->addQuotes( $params['excludeuser'] ) );
}
- if(!is_null($params['continue']) && ($mode == 'all' || $mode == 'revs'))
+ if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) )
{
- $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");
- $ns = intval($cont[0]);
- $title = $this->getDB()->strencode($this->titleToKey($cont[1]));
- $ts = $this->getDB()->strencode($cont[2]);
- $op = ($params['dir'] == 'newer' ? '>' : '<');
- $this->addWhere("ar_namespace $op $ns OR " .
+ $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" );
+ $ns = intval( $cont[0] );
+ $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $ts = $this->getDB()->strencode( $cont[2] );
+ $op = ( $params['dir'] == 'newer' ? '>' : '<' );
+ $this->addWhere( "ar_namespace $op $ns OR " .
"(ar_namespace = $ns AND " .
"(ar_title $op '$title' OR " .
"(ar_title = '$title' AND " .
- "ar_timestamp = '$ts')))");
+ "ar_timestamp $op= '$ts')))" );
}
- $this->addOption('LIMIT', $limit + 1);
- $this->addOption('USE INDEX', array('archive' => ($mode == 'user' ? 'usertext_timestamp' : 'name_title_timestamp')));
- if($mode == 'all')
- {
- if($params['unique'])
+ $this->addOption( 'LIMIT', $limit + 1 );
+ $this->addOption( 'USE INDEX', array( 'archive' => ( $mode == 'user' ? 'usertext_timestamp' : 'name_title_timestamp' ) ) );
+ if ( $mode == 'all' ) {
+ if ( $params['unique'] )
{
- $this->addOption('GROUP BY', 'ar_title');
- $this->addOption('ORDER BY', 'ar_title');
- }
- else
- $this->addOption('ORDER BY', 'ar_title, ar_timestamp');
- }
- else
- {
- if($mode == 'revs')
+ $this->addOption( 'GROUP BY', 'ar_title' );
+ $this->addOption( 'ORDER BY', 'ar_title' );
+ } else
+ $this->addOption( 'ORDER BY', 'ar_title, ar_timestamp' );
+ } else {
+ if ( $mode == 'revs' )
{
// Sort by ns and title in the same order as timestamp for efficiency
- $this->addWhereRange('ar_namespace', $params['dir'], null, null);
- $this->addWhereRange('ar_title', $params['dir'], null, null);
+ $this->addWhereRange( 'ar_namespace', $params['dir'], null, null );
+ $this->addWhereRange( 'ar_title', $params['dir'], null, null );
}
- $this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']);
+ $this->addWhereRange( 'ar_timestamp', $params['dir'], $params['start'], $params['end'] );
}
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$pageMap = array(); // Maps ns&title to (fake) pageid
$count = 0;
$newPageID = 0;
- while($row = $db->fetchObject($res))
+ while ( $row = $db->fetchObject( $res ) )
{
- if(++$count > $limit)
- {
+ if ( ++$count > $limit ) {
// We've had enough
- if($mode == 'all' || $mode == 'revs')
- $this->setContinueEnumParameter('continue', intval($row->ar_namespace) . '|' .
- $this->keyToTitle($row->ar_title) . '|' . $row->ar_timestamp);
+ if ( $mode == 'all' || $mode == 'revs' )
+ $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' .
+ $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp );
else
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) );
break;
}
$rev = array();
- $rev['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ar_timestamp);
- if($fld_revid)
- $rev['revid'] = intval($row->ar_rev_id);
- if($fld_user)
+ $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
+ if ( $fld_revid )
+ $rev['revid'] = intval( $row->ar_rev_id );
+ if ( $fld_user )
$rev['user'] = $row->ar_user_text;
- if($fld_comment)
+ if ( $fld_comment )
$rev['comment'] = $row->ar_comment;
- if($fld_minor)
- if($row->ar_minor_edit == 1)
- $rev['minor'] = '';
- if($fld_len)
+
+ $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+
+ if ( $fld_parsedcomment ) {
+ global $wgUser;
+ $rev['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->ar_comment, $title );
+ }
+ if ( $fld_minor && $row->ar_minor_edit == 1 )
+ $rev['minor'] = '';
+ if ( $fld_len )
$rev['len'] = $row->ar_len;
- if($fld_content)
- ApiResult::setContent($rev, Revision::getRevisionText($row));
+ if ( $fld_content )
+ ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
- if(!isset($pageMap[$row->ar_namespace][$row->ar_title]))
- {
+ if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
$pageID = $newPageID++;
$pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
- $t = Title::makeTitle($row->ar_namespace, $row->ar_title);
- $a['revisions'] = array($rev);
- $result->setIndexedTagName($a['revisions'], 'rev');
- ApiQueryBase::addTitleInfo($a, $t);
- if($fld_token)
+ $a['revisions'] = array( $rev );
+ $result->setIndexedTagName( $a['revisions'], 'rev' );
+ ApiQueryBase::addTitleInfo( $a, $title );
+ if ( $fld_token )
$a['token'] = $token;
- $fit = $result->addValue(array('query', $this->getModuleName()), $pageID, $a);
- }
- else
- {
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), $pageID, $a );
+ } else {
$pageID = $pageMap[$row->ar_namespace][$row->ar_title];
$fit = $result->addValue(
- array('query', $this->getModuleName(), $pageID, 'revisions'),
- null, $rev);
+ array( 'query', $this->getModuleName(), $pageID, 'revisions' ),
+ null, $rev );
}
- if(!$fit)
- {
- if($mode == 'all' || $mode == 'revs')
- $this->setContinueEnumParameter('continue', intval($row->ar_namespace) . '|' .
- $this->keyToTitle($row->ar_title) . '|' . $row->ar_timestamp);
+ if ( !$fit ) {
+ if ( $mode == 'all' || $mode == 'revs' )
+ $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' .
+ $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp );
else
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) );
break;
}
}
- $db->freeResult($res);
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'page');
+ $db->freeResult( $res );
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
}
public function getAllowedParams() {
@@ -284,6 +277,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
'revid',
'user',
'comment',
+ 'parsedcomment',
'minor',
'len',
'content',
@@ -320,6 +314,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
'For instance, a parameter marked (1) only applies to mode 1 and is ignored in modes 2 and 3.',
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revision information' ),
+ array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ),
+ array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revision content' ),
+ array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -335,6 +338,6 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 69578 2010-07-20 02:46:20Z tstarling $';
}
} \ No newline at end of file
diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php
index 50825464..4bd3f5fd 100644
--- a/includes/api/ApiQueryDisabled.php
+++ b/includes/api/ApiQueryDisabled.php
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
@@ -40,12 +40,12 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryDisabled extends ApiQueryBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
- $this->setWarning("The ``{$this->getModuleName()}'' module has been disabled.");
+ $this->setWarning( "The ``{$this->getModuleName()}'' module has been disabled." );
}
public function getAllowedParams() {
@@ -67,6 +67,6 @@ class ApiQueryDisabled extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryDisabled.php 41268 2008-09-25 20:50:50Z catrope $';
+ return __CLASS__ . ': $Id: ApiQueryDisabled.php 60930 2010-01-11 15:55:52Z simetrical $';
}
}
diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php
index a59ee356..ed070069 100644
--- a/includes/api/ApiQueryDuplicateFiles.php
+++ b/includes/api/ApiQueryDuplicateFiles.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'df');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'df' );
}
public function execute() {
@@ -47,11 +47,11 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
$namespaces = $this->getPageSet()->getAllTitlesByNamespace();
if ( empty( $namespaces[NS_FILE] ) ) {
@@ -59,71 +59,74 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
$images = $namespaces[NS_FILE];
- $this->addTables('image', 'i1');
- $this->addTables('image', 'i2');
- $this->addFields(array(
+ $this->addTables( 'image', 'i1' );
+ $this->addTables( 'image', 'i2' );
+ $this->addFields( array(
'i1.img_name AS orig_name',
'i2.img_name AS dup_name',
'i2.img_user_text AS dup_user_text',
'i2.img_timestamp AS dup_timestamp'
- ));
- $this->addWhere(array(
- 'i1.img_name' => array_keys($images),
+ ) );
+
+ $this->addWhere( array(
+ 'i1.img_name' => array_keys( $images ),
'i1.img_sha1 = i2.img_sha1',
'i1.img_name != i2.img_name',
- ));
- if(isset($params['continue']))
+ ) );
+
+ if ( isset( $params['continue'] ) )
{
- $cont = explode('|', $params['continue']);
- if(count($cont) != 2)
- $this->dieUsage("Invalid continue param. You should pass the " .
- "original value returned by the previous query", "_badcontinue");
- $orig = $this->getDB()->strencode($this->titleTokey($cont[0]));
- $dup = $this->getDB()->strencode($this->titleToKey($cont[1]));
- $this->addWhere("i1.img_name > '$orig' OR ".
- "(i1.img_name = '$orig' AND ".
- "i2.img_name >= '$dup')");
+ $cont = explode( '|', $params['continue'] );
+ if ( count( $cont ) != 2 )
+ $this->dieUsage( "Invalid continue param. You should pass the " .
+ "original value returned by the previous query", "_badcontinue" );
+ $orig = $this->getDB()->strencode( $this->titleTokey( $cont[0] ) );
+ $dup = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $this->addWhere( "i1.img_name > '$orig' OR " .
+ "(i1.img_name = '$orig' AND " .
+ "i2.img_name >= '$dup')" );
}
- $this->addOption('ORDER BY', 'i1.img_name');
- $this->addOption('LIMIT', $params['limit'] + 1);
- $res = $this->select(__METHOD__);
+ $this->addOption( 'ORDER BY', 'i1.img_name' );
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+
+ $res = $this->select( __METHOD__ );
$db = $this->getDB();
$count = 0;
$titles = array();
- while($row = $db->fetchObject($res))
+ while ( $row = $db->fetchObject( $res ) )
{
- if(++$count > $params['limit'])
+ if ( ++$count > $params['limit'] )
{
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('continue',
- $this->keyToTitle($row->orig_name) . '|' .
- $this->keyToTitle($row->dup_name));
+ $this->setContinueEnumParameter( 'continue',
+ $this->keyToTitle( $row->orig_name ) . '|' .
+ $this->keyToTitle( $row->dup_name ) );
break;
}
- if(!is_null($resultPageSet))
- $titles[] = Title::makeTitle(NS_FILE, $row->dup_name);
+ if ( !is_null( $resultPageSet ) )
+ $titles[] = Title::makeTitle( NS_FILE, $row->dup_name );
else
{
$r = array(
'name' => $row->dup_name,
'user' => $row->dup_user_text,
- 'timestamp' => wfTimestamp(TS_ISO_8601, $row->dup_timestamp)
+ 'timestamp' => wfTimestamp( TS_ISO_8601, $row->dup_timestamp )
);
- $fit = $this->addPageSubItem($images[$row->orig_name], $r);
- if(!$fit)
+ $fit = $this->addPageSubItem( $images[$row->orig_name], $r );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue',
- $this->keyToTitle($row->orig_name) . '|' .
- $this->keyToTitle($row->dup_name));
+ $this->setContinueEnumParameter( 'continue',
+ $this->keyToTitle( $row->orig_name ) . '|' .
+ $this->keyToTitle( $row->dup_name ) );
break;
}
}
}
- if(!is_null($resultPageSet))
- $resultPageSet->populateFromTitles($titles);
- $db->freeResult($res);
+ if ( !is_null( $resultPageSet ) )
+ $resultPageSet->populateFromTitles( $titles );
+ $db->freeResult( $res );
}
public function getAllowedParams() {
@@ -149,6 +152,12 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
public function getDescription() {
return 'List all files that are duplicates of the given file(s).';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
return array ( 'api.php?action=query&titles=File:Albert_Einstein_Head.jpg&prop=duplicatefiles',
@@ -157,6 +166,6 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryDuplicateFiles.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryDuplicateFiles.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index 08f6ab1f..0e171e44 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -33,8 +33,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'eu');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'eu' );
}
public function execute() {
@@ -45,11 +45,11 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
@@ -58,10 +58,10 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
// Find the right prefix
global $wgUrlProtocols;
- if($protocol && !in_array($protocol, $wgUrlProtocols))
+ if ( $protocol && !in_array( $protocol, $wgUrlProtocols ) )
{
- foreach ($wgUrlProtocols as $p) {
- if( substr( $p, 0, strlen( $protocol ) ) === $protocol ) {
+ foreach ( $wgUrlProtocols as $p ) {
+ if ( substr( $p, 0, strlen( $protocol ) ) === $protocol ) {
$protocol = $p;
break;
}
@@ -71,91 +71,92 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
$protocol = null;
$db = $this->getDB();
- $this->addTables(array('page','externallinks')); // must be in this order for 'USE INDEX'
- $this->addOption('USE INDEX', 'el_index');
- $this->addWhere('page_id=el_from');
- $this->addWhereFld('page_namespace', $params['namespace']);
+ $this->addTables( array( 'page', 'externallinks' ) ); // must be in this order for 'USE INDEX'
+ $this->addOption( 'USE INDEX', 'el_index' );
+ $this->addWhere( 'page_id=el_from' );
+ $this->addWhereFld( 'page_namespace', $params['namespace'] );
- if(!is_null($query) || $query != '')
+ if ( !is_null( $query ) || $query != '' )
{
- if(is_null($protocol))
+ 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 ));
+ $likeQuery = LinkFilter::makeLikeArray( $query, $protocol );
+ if ( !$likeQuery )
+ $this->dieUsage( 'Invalid query', 'bad_query' );
+
+ $likeQuery = LinkFilter::keepOneWildcard( $likeQuery );
+ $this->addWhere( 'el_index ' . $db->buildLike( $likeQuery ) );
}
- else if(!is_null($protocol))
- $this->addWhere('el_index LIKE ' . $db->addQuotes( "$protocol%" ));
+ else if ( !is_null( $protocol ) )
+ $this->addWhere( 'el_index ' . $db->buildLike( "$protocol", $db->anyString() ) );
- $prop = array_flip($params['prop']);
- $fld_ids = isset($prop['ids']);
- $fld_title = isset($prop['title']);
- $fld_url = isset($prop['url']);
+ $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 (
+ 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());
+ $this->addFields( $resultPageSet->getPageTableFields() );
}
$limit = $params['limit'];
$offset = $params['offset'];
- $this->addOption('LIMIT', $limit +1);
- if (isset ($offset))
- $this->addOption('OFFSET', $offset);
+ $this->addOption( 'LIMIT', $limit + 1 );
+ if ( isset ( $offset ) )
+ $this->addOption( 'OFFSET', $offset );
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$result = $this->getResult();
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ 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);
+ $this->setContinueEnumParameter( 'offset', $offset + $limit );
break;
}
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$vals = array();
- if ($fld_ids)
- $vals['pageid'] = intval($row->page_id);
- if ($fld_title) {
- $title = Title :: makeTitle($row->page_namespace, $row->page_title);
- ApiQueryBase::addTitleInfo($vals, $title);
+ if ( $fld_ids )
+ $vals['pageid'] = intval( $row->page_id );
+ if ( $fld_title ) {
+ $title = Title :: makeTitle( $row->page_namespace, $row->page_title );
+ ApiQueryBase::addTitleInfo( $vals, $title );
}
- if ($fld_url)
+ if ( $fld_url )
$vals['url'] = $row->el_to;
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('offset', $offset + $count - 1);
+ $this->setContinueEnumParameter( 'offset', $offset + $count - 1 );
break;
}
} else {
- $resultPageSet->processDbRow($row);
+ $resultPageSet->processDbRow( $row );
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()),
- $this->getModulePrefix());
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ),
+ $this->getModulePrefix() );
}
}
public function getAllowedParams() {
global $wgUrlProtocols;
- $protocols = array('');
- foreach ($wgUrlProtocols as $p) {
- $protocols[] = substr($p, 0, strpos($p,':'));
+ $protocols = array( '' );
+ foreach ( $wgUrlProtocols as $p ) {
+ $protocols[] = substr( $p, 0, strpos( $p, ':' ) );
}
return array (
@@ -195,7 +196,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
'prop' => 'What pieces of information to include',
'offset' => 'Used for paging. Use the value returned for "continue"',
'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'),
+ '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 pages to return.'
@@ -205,6 +206,12 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
public function getDescription() {
return 'Enumerate pages that contain a given URL';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'bad_query', 'info' => 'Invalid query' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -213,6 +220,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
index 0bddd6df..a748e036 100644
--- a/includes/api/ApiQueryExternalLinks.php
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryExternalLinks extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'el');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'el' );
}
public function execute() {
@@ -44,41 +44,43 @@ class ApiQueryExternalLinks extends ApiQueryBase {
return;
$params = $this->extractRequestParams();
- $this->addFields(array (
+ $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']);
+ $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__);
+ $res = $this->select( __METHOD__ );
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if (++$count > $params['limit']) {
+ 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']);
+ $this->setContinueEnumParameter( 'offset', @$params['offset'] + $params['limit'] );
break;
}
$entry = array();
- ApiResult :: setContent($entry, $row->el_to);
- $fit = $this->addPageSubItem($row->el_from, $entry);
- if(!$fit)
+ ApiResult :: setContent( $entry, $row->el_to );
+ $fit = $this->addPageSubItem( $row->el_from, $entry );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('offset', @$params['offset'] + $count - 1);
+ $this->setContinueEnumParameter( 'offset', @$params['offset'] + $count - 1 );
break;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
public function getCacheMode( $params ) {
@@ -117,6 +119,6 @@ class ApiQueryExternalLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index c4c71075..3704710a 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,19 +35,19 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryImageInfo extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'ii');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'ii' );
}
public function execute() {
$params = $this->extractRequestParams();
- $prop = array_flip($params['prop']);
+ $prop = array_flip( $params['prop'] );
- if($params['urlheight'] != -1 && $params['urlwidth'] == -1)
- $this->dieUsage("iiurlheight cannot be used without iiurlwidth", 'iiurlwidth');
+ if ( $params['urlheight'] != - 1 && $params['urlwidth'] == - 1 )
+ $this->dieUsage( "iiurlheight cannot be used without iiurlwidth", 'iiurlwidth' );
- if ( $params['urlwidth'] != -1 ) {
+ if ( $params['urlwidth'] != - 1 ) {
$scale = array();
$scale['width'] = $params['urlwidth'];
$scale['height'] = $params['urlheight'];
@@ -57,23 +57,23 @@ class ApiQueryImageInfo extends ApiQueryBase {
$pageIds = $this->getPageSet()->getAllTitlesByNamespace();
if ( !empty( $pageIds[NS_FILE] ) ) {
- $titles = array_keys($pageIds[NS_FILE]);
- asort($titles); // Ensure the order is always the same
+ $titles = array_keys( $pageIds[NS_FILE] );
+ asort( $titles ); // Ensure the order is always the same
$skip = false;
- if(!is_null($params['continue']))
+ if ( !is_null( $params['continue'] ) )
{
$skip = true;
- $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");
- $fromTitle = strval($cont[0]);
+ $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" );
+ $fromTitle = strval( $cont[0] );
$fromTimestamp = $cont[1];
// Filter out any titles before $fromTitle
- foreach($titles as $key => $title)
- if($title < $fromTitle)
- unset($titles[$key]);
+ foreach ( $titles as $key => $title )
+ if ( $title < $fromTitle )
+ unset( $titles[$key] );
else
break;
}
@@ -81,90 +81,95 @@ class ApiQueryImageInfo extends ApiQueryBase {
$result = $this->getResult();
$images = RepoGroup::singleton()->findFiles( $titles );
foreach ( $images as $img ) {
+ // Skip redirects
+ if ( $img->getOriginalTitle()->isRedirect() )
+ continue;
+
$start = $skip ? $fromTimestamp : $params['start'];
$pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ];
$fit = $result->addValue(
- array('query', 'pages', intval($pageId)),
+ array( 'query', 'pages', intval( $pageId ) ),
'imagerepository', $img->getRepoName()
);
- if(!$fit)
+ if ( !$fit )
{
- if(count($pageIds[NS_IMAGE]) == 1)
- # The user is screwed. imageinfo can't be solely
- # responsible for exceeding the limit in this case,
- # so set a query-continue that just returns the same
- # thing again. When the violating queries have been
- # out-continued, the result will get through
- $this->setContinueEnumParameter('start',
- wfTimestamp(TS_ISO_8601, $img->getTimestamp()));
+ if ( count( $pageIds[NS_IMAGE] ) == 1 )
+ // The user is screwed. imageinfo can't be solely
+ // responsible for exceeding the limit in this case,
+ // so set a query-continue that just returns the same
+ // thing again. When the violating queries have been
+ // out-continued, the result will get through
+ $this->setContinueEnumParameter( 'start',
+ wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) );
else
- $this->setContinueEnumParameter('continue',
- $this->getContinueStr($img));
+ $this->setContinueEnumParameter( 'continue',
+ $this->getContinueStr( $img ) );
break;
}
// Get information about the current version first
// Check that the current version is within the start-end boundaries
$gotOne = false;
- if((is_null($start) || $img->getTimestamp() <= $start) &&
- (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) {
+ if ( ( is_null( $start ) || $img->getTimestamp() <= $start ) &&
+ ( is_null( $params['end'] ) || $img->getTimestamp() >= $params['end'] ) ) {
$gotOne = true;
- $fit = $this->addPageSubItem($pageId,
- self::getInfo( $img, $prop, $result, $scale));
- if(!$fit)
+ $fit = $this->addPageSubItem( $pageId,
+ self::getInfo( $img, $prop, $result, $scale ) );
+ if ( !$fit )
{
- if(count($pageIds[NS_IMAGE]) == 1)
- # See the 'the user is screwed' comment above
- $this->setContinueEnumParameter('start',
- wfTimestamp(TS_ISO_8601, $img->getTimestamp()));
+ if ( count( $pageIds[NS_IMAGE] ) == 1 )
+ // See the 'the user is screwed' comment above
+ $this->setContinueEnumParameter( 'start',
+ wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) );
else
- $this->setContinueEnumParameter('continue',
- $this->getContinueStr($img));
+ $this->setContinueEnumParameter( 'continue',
+ $this->getContinueStr( $img ) );
break;
}
}
// Now get the old revisions
// Get one more to facilitate query-continue functionality
- $count = ($gotOne ? 1 : 0);
- $oldies = $img->getHistory($params['limit'] - $count + 1, $start, $params['end']);
- foreach($oldies as $oldie) {
- if(++$count > $params['limit']) {
+ $count = ( $gotOne ? 1 : 0 );
+ $oldies = $img->getHistory( $params['limit'] - $count + 1, $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_FILE]) == 1)
+ if ( count( $pageIds[NS_FILE] ) == 1 )
{
- $this->setContinueEnumParameter('start',
- wfTimestamp(TS_ISO_8601, $oldie->getTimestamp()));
+ $this->setContinueEnumParameter( 'start',
+ wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) );
}
break;
}
- $fit = $this->addPageSubItem($pageId,
- self::getInfo($oldie, $prop, $result));
- if(!$fit)
+ $fit = $this->addPageSubItem( $pageId,
+ self::getInfo( $oldie, $prop, $result ) );
+ if ( !$fit )
{
- if(count($pageIds[NS_IMAGE]) == 1)
- $this->setContinueEnumParameter('start',
- wfTimestamp(TS_ISO_8601, $oldie->getTimestamp()));
+ if ( count( $pageIds[NS_IMAGE] ) == 1 )
+ $this->setContinueEnumParameter( 'start',
+ wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) );
else
- $this->setContinueEnumParameter('continue',
- $this->getContinueStr($oldie));
+ $this->setContinueEnumParameter( 'continue',
+ $this->getContinueStr( $oldie ) );
break;
}
}
- if(!$fit)
+ if ( !$fit )
break;
$skip = false;
}
- $missing = array_diff( array_keys( $pageIds[NS_FILE] ), array_keys( $images ) );
- foreach ($missing as $title) {
- $result->addValue(
- array('query', 'pages', intval($pageIds[NS_FILE][$title])),
- 'imagerepository', ''
- );
- // The above can't fail because it doesn't increase the result size
+ $data = $this->getResultData();
+ foreach ( $data['query']['pages'] as $pageid => $arr ) {
+ if ( !isset( $arr['imagerepository'] ) )
+ $result->addValue(
+ array( 'query', 'pages', $pageid ),
+ 'imagerepository', ''
+ );
+ // The above can't fail because it doesn't increase the result size
}
}
}
@@ -174,26 +179,26 @@ class ApiQueryImageInfo extends ApiQueryBase {
* @param File f The image
* @return array Result array
*/
- static function getInfo($file, $prop, $result, $scale = null) {
+ static function getInfo( $file, $prop, $result, $scale = null ) {
$vals = array();
- if( isset( $prop['timestamp'] ) )
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $file->getTimestamp());
- if( isset( $prop['user'] ) ) {
+ if ( isset( $prop['timestamp'] ) )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
+ if ( isset( $prop['user'] ) ) {
$vals['user'] = $file->getUser();
- if( !$file->getUser( 'id' ) )
+ if ( !$file->getUser( 'id' ) )
$vals['anon'] = '';
}
- if( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) {
+ if ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) {
$vals['size'] = intval( $file->getSize() );
$vals['width'] = intval( $file->getWidth() );
$vals['height'] = intval( $file->getHeight() );
}
- if( isset( $prop['url'] ) ) {
- if( !is_null( $scale ) && !$file->isOld() ) {
+ if ( isset( $prop['url'] ) ) {
+ if ( !is_null( $scale ) && !$file->isOld() ) {
$mto = $file->transform( array( 'width' => $scale['width'], 'height' => $scale['height'] ) );
- if( $mto && !$mto->isError() )
+ if ( $mto && !$mto->isError() )
{
- $vals['thumburl'] = $mto->getUrl();
+ $vals['thumburl'] = wfExpandUrl( $mto->getUrl() );
$vals['thumbwidth'] = intval( $mto->getWidth() );
$vals['thumbheight'] = intval( $mto->getHeight() );
}
@@ -201,41 +206,41 @@ class ApiQueryImageInfo extends ApiQueryBase {
$vals['url'] = $file->getFullURL();
$vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl() );
}
- if( isset( $prop['comment'] ) )
+ if ( isset( $prop['comment'] ) )
$vals['comment'] = $file->getDescription();
- if( isset( $prop['sha1'] ) )
+ if ( isset( $prop['sha1'] ) )
$vals['sha1'] = wfBaseConvert( $file->getSha1(), 36, 16, 40 );
- if( isset( $prop['metadata'] ) ) {
+ if ( isset( $prop['metadata'] ) ) {
$metadata = $file->getMetadata();
$vals['metadata'] = $metadata ? self::processMetaData( unserialize( $metadata ), $result ) : null;
}
- if( isset( $prop['mime'] ) )
+ if ( isset( $prop['mime'] ) )
$vals['mime'] = $file->getMimeType();
- if( isset( $prop['archivename'] ) && $file->isOld() )
+ if ( isset( $prop['archivename'] ) && $file->isOld() )
$vals['archivename'] = $file->getArchiveName();
- if( isset( $prop['bitdepth'] ) )
+ if ( isset( $prop['bitdepth'] ) )
$vals['bitdepth'] = $file->getBitDepth();
return $vals;
}
- public static function processMetaData($metadata, $result)
+ public static function processMetaData( $metadata, $result )
{
$retval = array();
if ( is_array( $metadata ) ) {
- foreach($metadata as $key => $value)
+ foreach ( $metadata as $key => $value )
{
- $r = array('name' => $key);
- if(is_array($value))
- $r['value'] = self::processMetaData($value, $result);
+ $r = array( 'name' => $key );
+ if ( is_array( $value ) )
+ $r['value'] = self::processMetaData( $value, $result );
else
$r['value'] = $value;
$retval[] = $r;
}
}
- $result->setIndexedTagName($retval, 'metadata');
+ $result->setIndexedTagName( $retval, 'metadata' );
return $retval;
}
@@ -243,7 +248,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
return 'public';
}
- private function getContinueStr($img)
+ private function getContinueStr( $img )
{
return $img->getOriginalTitle()->getText() .
'|' . $img->getTimestamp();
@@ -254,18 +259,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_DFLT => 'timestamp|user',
- ApiBase :: PARAM_TYPE => array (
- 'timestamp',
- 'user',
- 'comment',
- 'url',
- 'size',
- 'sha1',
- 'mime',
- 'metadata',
- 'archivename',
- 'bitdepth',
- )
+ ApiBase :: PARAM_TYPE => self::getPropertyNames()
),
'limit' => array(
ApiBase :: PARAM_TYPE => 'limit',
@@ -282,15 +276,34 @@ class ApiQueryImageInfo extends ApiQueryBase {
),
'urlwidth' => array(
ApiBase :: PARAM_TYPE => 'integer',
- ApiBase :: PARAM_DFLT => -1
+ ApiBase :: PARAM_DFLT => - 1
),
'urlheight' => array(
ApiBase :: PARAM_TYPE => 'integer',
- ApiBase :: PARAM_DFLT => -1
+ ApiBase :: PARAM_DFLT => - 1
),
'continue' => null,
);
}
+
+ /**
+ * Returns all possible parameters to iiprop
+ */
+ public static function getPropertyNames() {
+ return array (
+ 'timestamp',
+ 'user',
+ 'comment',
+ 'url',
+ 'size',
+ 'dimensions', // For backwards compatibility with Allimages
+ 'sha1',
+ 'mime',
+ 'metadata',
+ 'archivename',
+ 'bitdepth',
+ );
+ }
public function getParamDescription() {
return array (
@@ -298,8 +311,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' => 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.'),
+ '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',
'continue' => 'When more results are available, use this to continue',
);
@@ -310,6 +323,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
'Returns image information and upload history'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'iiurlwidth', 'info' => 'iiurlheight cannot be used without iiurlwidth' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -319,6 +338,6 @@ class ApiQueryImageInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImageInfo.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryImageInfo.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php
index 9dbe08a6..65df94dc 100644
--- a/includes/api/ApiQueryImages.php
+++ b/includes/api/ApiQueryImages.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,69 +35,70 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryImages extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'im');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'im' );
}
public function execute() {
$this->run();
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
- if ($this->getPageSet()->getGoodTitleCount() == 0)
+ if ( $this->getPageSet()->getGoodTitleCount() == 0 )
return; // nothing to do
$params = $this->extractRequestParams();
- $this->addFields(array (
+ $this->addFields( array (
'il_from',
'il_to'
- ));
-
- $this->addTables('imagelinks');
- $this->addWhereFld('il_from', array_keys($this->getPageSet()->getGoodTitles()));
- 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')");
+ ) );
+
+ $this->addTables( 'imagelinks' );
+ $this->addWhereFld( 'il_from', array_keys( $this->getPageSet()->getGoodTitles() ) );
+ 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');
+
+ // 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);
+ $this->addOption( 'ORDER BY', 'il_from, il_to' );
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if (++$count > $params['limit']) {
+ 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));
+ $this->setContinueEnumParameter( 'continue', $row->il_from .
+ '|' . $this->keyToTitle( $row->il_to ) );
break;
}
$vals = array();
- ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_FILE, $row->il_to));
- $fit = $this->addPageSubItem($row->il_from, $vals);
- if(!$fit)
+ ApiQueryBase :: addTitleInfo( $vals, Title :: makeTitle( NS_FILE, $row->il_to ) );
+ $fit = $this->addPageSubItem( $row->il_from, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue', $row->il_from .
- '|' . $this->keyToTitle($row->il_to));
+ $this->setContinueEnumParameter( 'continue', $row->il_from .
+ '|' . $this->keyToTitle( $row->il_to ) );
break;
}
}
@@ -105,20 +106,20 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
$titles = array();
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if (++$count > $params['limit']) {
+ 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));
+ $this->setContinueEnumParameter( 'continue', $row->il_from .
+ '|' . $this->keyToTitle( $row->il_to ) );
break;
}
- $titles[] = Title :: makeTitle(NS_FILE, $row->il_to);
+ $titles[] = Title :: makeTitle( NS_FILE, $row->il_to );
}
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
public function getCacheMode( $params ) {
@@ -148,6 +149,12 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
public function getDescription() {
return 'Returns all images contained on the given page(s)';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -159,6 +166,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryImages.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryImages.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index f78450b7..b1c2963c 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -34,23 +34,24 @@ if (!defined('MEDIAWIKI')) {
* @ingroup API
*/
class ApiQueryInfo extends ApiQueryBase {
-
+
private $fld_protection = false, $fld_talkid = false,
$fld_subjectid = false, $fld_url = false,
- $fld_readable = false;
+ $fld_readable = false, $fld_watched = false,
+ $fld_preload = false;
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'in');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'in' );
}
- public function requestExtraData($pageSet) {
- $pageSet->requestField('page_restrictions');
- $pageSet->requestField('page_is_redirect');
- $pageSet->requestField('page_is_new');
- $pageSet->requestField('page_counter');
- $pageSet->requestField('page_touched');
- $pageSet->requestField('page_latest');
- $pageSet->requestField('page_len');
+ public function requestExtraData( $pageSet ) {
+ $pageSet->requestField( 'page_restrictions' );
+ $pageSet->requestField( 'page_is_redirect' );
+ $pageSet->requestField( 'page_is_new' );
+ $pageSet->requestField( 'page_counter' );
+ $pageSet->requestField( 'page_touched' );
+ $pageSet->requestField( 'page_latest' );
+ $pageSet->requestField( 'page_len' );
}
/**
@@ -61,11 +62,11 @@ class ApiQueryInfo extends ApiQueryBase {
*/
protected function getTokenFunctions() {
// Don't call the hooks twice
- if(isset($this->tokenFunctions))
+ 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')))
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) )
return array();
$this->tokenFunctions = array(
@@ -78,112 +79,112 @@ class ApiQueryInfo extends ApiQueryBase {
'email' => array( 'ApiQueryInfo', 'getEmailToken' ),
'import' => array( 'ApiQueryInfo', 'getImportToken' ),
);
- wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions));
+ wfRunHooks( 'APIQueryInfoTokens', array( &$this->tokenFunctions ) );
return $this->tokenFunctions;
}
- public static function getEditToken($pageid, $title)
+ public static function getEditToken( $pageid, $title )
{
// We could check for $title->userCan('edit') here,
// but that's too expensive for this purpose
// and would break caching
global $wgUser;
- if(!$wgUser->isAllowed('edit'))
+ if ( !$wgUser->isAllowed( 'edit' ) )
return false;
-
+
// The edit token is always the same, let's exploit that
static $cachedEditToken = null;
- if(!is_null($cachedEditToken))
+ if ( !is_null( $cachedEditToken ) )
return $cachedEditToken;
$cachedEditToken = $wgUser->editToken();
return $cachedEditToken;
}
-
- public static function getDeleteToken($pageid, $title)
+
+ public static function getDeleteToken( $pageid, $title )
{
global $wgUser;
- if(!$wgUser->isAllowed('delete'))
- return false;
+ if ( !$wgUser->isAllowed( 'delete' ) )
+ return false;
static $cachedDeleteToken = null;
- if(!is_null($cachedDeleteToken))
+ if ( !is_null( $cachedDeleteToken ) )
return $cachedDeleteToken;
$cachedDeleteToken = $wgUser->editToken();
return $cachedDeleteToken;
}
- public static function getProtectToken($pageid, $title)
+ public static function getProtectToken( $pageid, $title )
{
global $wgUser;
- if(!$wgUser->isAllowed('protect'))
+ if ( !$wgUser->isAllowed( 'protect' ) )
return false;
static $cachedProtectToken = null;
- if(!is_null($cachedProtectToken))
+ if ( !is_null( $cachedProtectToken ) )
return $cachedProtectToken;
$cachedProtectToken = $wgUser->editToken();
return $cachedProtectToken;
}
- public static function getMoveToken($pageid, $title)
+ public static function getMoveToken( $pageid, $title )
{
global $wgUser;
- if(!$wgUser->isAllowed('move'))
+ if ( !$wgUser->isAllowed( 'move' ) )
return false;
static $cachedMoveToken = null;
- if(!is_null($cachedMoveToken))
+ if ( !is_null( $cachedMoveToken ) )
return $cachedMoveToken;
$cachedMoveToken = $wgUser->editToken();
return $cachedMoveToken;
}
- public static function getBlockToken($pageid, $title)
+ public static function getBlockToken( $pageid, $title )
{
global $wgUser;
- if(!$wgUser->isAllowed('block'))
+ if ( !$wgUser->isAllowed( 'block' ) )
return false;
static $cachedBlockToken = null;
- if(!is_null($cachedBlockToken))
+ if ( !is_null( $cachedBlockToken ) )
return $cachedBlockToken;
$cachedBlockToken = $wgUser->editToken();
return $cachedBlockToken;
}
- public static function getUnblockToken($pageid, $title)
+ public static function getUnblockToken( $pageid, $title )
{
// Currently, this is exactly the same as the block token
- return self::getBlockToken($pageid, $title);
+ return self::getBlockToken( $pageid, $title );
}
- public static function getEmailToken($pageid, $title)
+ public static function getEmailToken( $pageid, $title )
{
global $wgUser;
- if(!$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser())
+ if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser() )
return false;
static $cachedEmailToken = null;
- if(!is_null($cachedEmailToken))
+ if ( !is_null( $cachedEmailToken ) )
return $cachedEmailToken;
$cachedEmailToken = $wgUser->editToken();
return $cachedEmailToken;
}
-
- public static function getImportToken($pageid, $title)
+
+ public static function getImportToken( $pageid, $title )
{
global $wgUser;
- if(!$wgUser->isAllowed('import'))
+ if ( !$wgUser->isAllowed( 'import' ) )
return false;
static $cachedImportToken = null;
- if(!is_null($cachedImportToken))
+ if ( !is_null( $cachedImportToken ) )
return $cachedImportToken;
$cachedImportToken = $wgUser->editToken();
@@ -192,13 +193,15 @@ class ApiQueryInfo extends ApiQueryBase {
public function execute() {
$this->params = $this->extractRequestParams();
- if(!is_null($this->params['prop'])) {
- $prop = array_flip($this->params['prop']);
- $this->fld_protection = isset($prop['protection']);
- $this->fld_talkid = isset($prop['talkid']);
- $this->fld_subjectid = isset($prop['subjectid']);
- $this->fld_url = isset($prop['url']);
- $this->fld_readable = isset($prop['readable']);
+ if ( !is_null( $this->params['prop'] ) ) {
+ $prop = array_flip( $this->params['prop'] );
+ $this->fld_protection = isset( $prop['protection'] );
+ $this->fld_watched = isset( $prop['watched'] );
+ $this->fld_talkid = isset( $prop['talkid'] );
+ $this->fld_subjectid = isset( $prop['subjectid'] );
+ $this->fld_url = isset( $prop['url'] );
+ $this->fld_readable = isset( $prop['readable'] );
+ $this->fld_preload = isset ( $prop['preload'] );
}
$pageSet = $this->getPageSet();
@@ -207,54 +210,57 @@ class ApiQueryInfo extends ApiQueryBase {
$this->everything = $this->titles + $this->missing;
$result = $this->getResult();
- uasort($this->everything, array('Title', 'compare'));
- if(!is_null($this->params['continue']))
+ uasort( $this->everything, array( 'Title', 'compare' ) );
+ if ( !is_null( $this->params['continue'] ) )
{
// Throw away any titles we're gonna skip so they don't
// clutter queries
- $cont = explode('|', $this->params['continue']);
- if(count($cont) != 2)
- $this->dieUsage("Invalid continue param. You should pass the original " .
- "value returned by the previous query", "_badcontinue");
- $conttitle = Title::makeTitleSafe($cont[0], $cont[1]);
- foreach($this->everything as $pageid => $title)
+ $cont = explode( '|', $this->params['continue'] );
+ if ( count( $cont ) != 2 )
+ $this->dieUsage( "Invalid continue param. You should pass the original " .
+ "value returned by the previous query", "_badcontinue" );
+ $conttitle = Title::makeTitleSafe( $cont[0], $cont[1] );
+ foreach ( $this->everything as $pageid => $title )
{
- if(Title::compare($title, $conttitle) >= 0)
+ if ( Title::compare( $title, $conttitle ) >= 0 )
break;
- unset($this->titles[$pageid]);
- unset($this->missing[$pageid]);
- unset($this->everything[$pageid]);
+ unset( $this->titles[$pageid] );
+ unset( $this->missing[$pageid] );
+ unset( $this->everything[$pageid] );
}
}
- $this->pageRestrictions = $pageSet->getCustomField('page_restrictions');
- $this->pageIsRedir = $pageSet->getCustomField('page_is_redirect');
- $this->pageIsNew = $pageSet->getCustomField('page_is_new');
- $this->pageCounter = $pageSet->getCustomField('page_counter');
- $this->pageTouched = $pageSet->getCustomField('page_touched');
- $this->pageLatest = $pageSet->getCustomField('page_latest');
- $this->pageLength = $pageSet->getCustomField('page_len');
+ $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' );
+ $this->pageIsRedir = $pageSet->getCustomField( 'page_is_redirect' );
+ $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' );
+ $this->pageCounter = $pageSet->getCustomField( 'page_counter' );
+ $this->pageTouched = $pageSet->getCustomField( 'page_touched' );
+ $this->pageLatest = $pageSet->getCustomField( 'page_latest' );
+ $this->pageLength = $pageSet->getCustomField( 'page_len' );
$db = $this->getDB();
// Get protection info if requested
- if ($this->fld_protection)
+ if ( $this->fld_protection )
$this->getProtectionInfo();
+ if ( $this->fld_watched )
+ $this->getWatchedInfo();
+
// Run the talkid/subjectid query if requested
- if($this->fld_talkid || $this->fld_subjectid)
+ if ( $this->fld_talkid || $this->fld_subjectid )
$this->getTSIDs();
- foreach($this->everything as $pageid => $title) {
- $pageInfo = $this->extractPageInfo($pageid, $title);
- $fit = $result->addValue(array (
+ foreach ( $this->everything as $pageid => $title ) {
+ $pageInfo = $this->extractPageInfo( $pageid, $title );
+ $fit = $result->addValue( array (
'query',
'pages'
- ), $pageid, $pageInfo);
- if(!$fit)
+ ), $pageid, $pageInfo );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue',
+ $this->setContinueEnumParameter( 'continue',
$title->getNamespace() . '|' .
- $title->getText());
+ $title->getText() );
break;
}
}
@@ -266,52 +272,67 @@ class ApiQueryInfo extends ApiQueryBase {
* @param $title Title object
* @return array
*/
- private function extractPageInfo($pageid, $title)
+ private function extractPageInfo( $pageid, $title )
{
$pageInfo = array();
- if($title->exists())
+ if ( $title->exists() )
{
- $pageInfo['touched'] = wfTimestamp(TS_ISO_8601, $this->pageTouched[$pageid]);
- $pageInfo['lastrevid'] = intval($this->pageLatest[$pageid]);
- $pageInfo['counter'] = intval($this->pageCounter[$pageid]);
- $pageInfo['length'] = intval($this->pageLength[$pageid]);
- if ($this->pageIsRedir[$pageid])
+ $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] );
+ $pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] );
+ $pageInfo['counter'] = intval( $this->pageCounter[$pageid] );
+ $pageInfo['length'] = intval( $this->pageLength[$pageid] );
+ if ( $this->pageIsRedir[$pageid] )
$pageInfo['redirect'] = '';
- if ($this->pageIsNew[$pageid])
+ if ( $this->pageIsNew[$pageid] )
$pageInfo['new'] = '';
}
- if (!is_null($this->params['token'])) {
+ if ( !is_null( $this->params['token'] ) ) {
$tokenFunctions = $this->getTokenFunctions();
- $pageInfo['starttimestamp'] = wfTimestamp(TS_ISO_8601, time());
- foreach($this->params['token'] as $t)
+ $pageInfo['starttimestamp'] = wfTimestamp( TS_ISO_8601, time() );
+ foreach ( $this->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");
+ $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($this->fld_protection) {
+ if ( $this->fld_protection ) {
$pageInfo['protection'] = array();
- if (isset($this->protections[$title->getNamespace()][$title->getDBkey()]))
+ if ( isset( $this->protections[$title->getNamespace()][$title->getDBkey()] ) )
$pageInfo['protection'] =
$this->protections[$title->getNamespace()][$title->getDBkey()];
- $this->getResult()->setIndexedTagName($pageInfo['protection'], 'pr');
+ $this->getResult()->setIndexedTagName( $pageInfo['protection'], 'pr' );
}
- if($this->fld_talkid && isset($this->talkids[$title->getNamespace()][$title->getDBKey()]))
- $pageInfo['talkid'] = $this->talkids[$title->getNamespace()][$title->getDBKey()];
- if($this->fld_subjectid && isset($this->subjectids[$title->getNamespace()][$title->getDBKey()]))
- $pageInfo['subjectid'] = $this->subjectids[$title->getNamespace()][$title->getDBKey()];
- if($this->fld_url) {
+
+ if ( $this->fld_watched && isset( $this->watched[$title->getNamespace()][$title->getDBkey()] ) )
+ $pageInfo['watched'] = '';
+
+ if ( $this->fld_talkid && isset( $this->talkids[$title->getNamespace()][$title->getDBkey()] ) )
+ $pageInfo['talkid'] = $this->talkids[$title->getNamespace()][$title->getDBkey()];
+
+ if ( $this->fld_subjectid && isset( $this->subjectids[$title->getNamespace()][$title->getDBkey()] ) )
+ $pageInfo['subjectid'] = $this->subjectids[$title->getNamespace()][$title->getDBkey()];
+
+ if ( $this->fld_url ) {
$pageInfo['fullurl'] = $title->getFullURL();
- $pageInfo['editurl'] = $title->getFullURL('action=edit');
+ $pageInfo['editurl'] = $title->getFullURL( 'action=edit' );
+ }
+ if ( $this->fld_readable && $title->userCanRead() )
+ $pageInfo['readable'] = '';
+
+ if ( $this->fld_preload ) {
+ if ( $title->exists() )
+ $pageInfo['preload'] = '';
+ else {
+ wfRunHooks( 'EditFormPreloadText', array( &$text, &$title ) );
+
+ $pageInfo['preload'] = $text;
+ }
}
- if($this->fld_readable)
- if($title->userCanRead())
- $pageInfo['readable'] = '';
return $pageInfo;
}
@@ -324,36 +345,37 @@ class ApiQueryInfo extends ApiQueryBase {
$db = $this->getDB();
// Get normal protections for existing titles
- if(count($this->titles))
+ if ( count( $this->titles ) )
{
- $this->addTables(array('page_restrictions', 'page'));
- $this->addWhere('page_id=pr_page');
- $this->addFields(array('pr_page', 'pr_type', 'pr_level',
+ $this->resetQueryParams();
+ $this->addTables( array( 'page_restrictions', 'page' ) );
+ $this->addWhere( 'page_id=pr_page' );
+ $this->addFields( array( 'pr_page', 'pr_type', 'pr_level',
'pr_expiry', 'pr_cascade', 'page_namespace',
- 'page_title'));
- $this->addWhereFld('pr_page', array_keys($this->titles));
+ 'page_title' ) );
+ $this->addWhereFld( 'pr_page', array_keys( $this->titles ) );
- $res = $this->select(__METHOD__);
- while($row = $db->fetchObject($res)) {
+ $res = $this->select( __METHOD__ );
+ while ( $row = $db->fetchObject( $res ) ) {
$a = array(
'type' => $row->pr_type,
'level' => $row->pr_level,
- 'expiry' => Block::decodeExpiry($row->pr_expiry, TS_ISO_8601)
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 )
);
- if($row->pr_cascade)
+ if ( $row->pr_cascade )
$a['cascade'] = '';
$this->protections[$row->page_namespace][$row->page_title][] = $a;
- # Also check old restrictions
- if($this->pageRestrictions[$row->pr_page]) {
- $restrictions = explode(':', trim($this->pageRestrictions[$row->pr_page]));
- foreach($restrictions as $restrict) {
- $temp = explode('=', trim($restrict));
- if(count($temp) == 1) {
+ // Also check old restrictions
+ if ( $this->pageRestrictions[$row->pr_page] ) {
+ $restrictions = explode( ':', trim( $this->pageRestrictions[$row->pr_page] ) );
+ foreach ( $restrictions as $restrict ) {
+ $temp = explode( '=', trim( $restrict ) );
+ if ( count( $temp ) == 1 ) {
// old old format should be treated as edit/move restriction
- $restriction = trim($temp[0]);
+ $restriction = trim( $temp[0] );
- if($restriction == '')
+ if ( $restriction == '' )
continue;
$this->protections[$row->page_namespace][$row->page_title][] = array(
'type' => 'edit',
@@ -366,8 +388,8 @@ class ApiQueryInfo extends ApiQueryBase {
'expiry' => 'infinity',
);
} else {
- $restriction = trim($temp[1]);
- if($restriction == '')
+ $restriction = trim( $temp[1] );
+ if ( $restriction == '' )
continue;
$this->protections[$row->page_namespace][$row->page_title][] = array(
'type' => $temp[0],
@@ -378,84 +400,84 @@ class ApiQueryInfo extends ApiQueryBase {
}
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
// Get protections for missing titles
- if(count($this->missing))
+ if ( count( $this->missing ) )
{
$this->resetQueryParams();
- $lb = new LinkBatch($this->missing);
- $this->addTables('protected_titles');
- $this->addFields(array('pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry'));
- $this->addWhere($lb->constructSet('pt', $db));
- $res = $this->select(__METHOD__);
- while($row = $db->fetchObject($res)) {
+ $lb = new LinkBatch( $this->missing );
+ $this->addTables( 'protected_titles' );
+ $this->addFields( array( 'pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry' ) );
+ $this->addWhere( $lb->constructSet( 'pt', $db ) );
+ $res = $this->select( __METHOD__ );
+ while ( $row = $db->fetchObject( $res ) ) {
$this->protections[$row->pt_namespace][$row->pt_title][] = array(
'type' => 'create',
'level' => $row->pt_create_perm,
- 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601)
+ 'expiry' => Block::decodeExpiry( $row->pt_expiry, TS_ISO_8601 )
);
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
// Cascading protections
$images = $others = array();
- foreach ($this->everything as $title)
- if ($title->getNamespace() == NS_FILE)
- $images[] = $title->getDBKey();
+ foreach ( $this->everything as $title )
+ if ( $title->getNamespace() == NS_FILE )
+ $images[] = $title->getDBkey();
else
$others[] = $title;
- if (count($others)) {
+ if ( count( $others ) ) {
// Non-images: check templatelinks
- $lb = new LinkBatch($others);
+ $lb = new LinkBatch( $others );
$this->resetQueryParams();
- $this->addTables(array('page_restrictions', 'page', 'templatelinks'));
- $this->addFields(array('pr_type', 'pr_level', 'pr_expiry',
+ $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);
+ '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 );
$this->protections[$row->tl_namespace][$row->tl_title][] = array(
'type' => $row->pr_type,
'level' => $row->pr_level,
- 'expiry' => Block::decodeExpiry($row->pr_expiry, TS_ISO_8601),
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
'source' => $source->getPrefixedText()
);
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
- if (count($images)) {
+ if ( count( $images ) ) {
// Images: check imagelinks
$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);
+ $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 );
$this->protections[NS_FILE][$row->il_to][] = array(
'type' => $row->pr_type,
'level' => $row->pr_level,
- 'expiry' => Block::decodeExpiry($row->pr_expiry, TS_ISO_8601),
+ 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ),
'source' => $source->getPrefixedText()
);
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
}
@@ -467,35 +489,67 @@ class ApiQueryInfo extends ApiQueryBase {
{
$getTitles = $this->talkids = $this->subjectids = array();
$db = $this->getDB();
- foreach($this->everything as $t)
+ foreach ( $this->everything as $t )
{
- if(MWNamespace::isTalk($t->getNamespace()))
+ if ( MWNamespace::isTalk( $t->getNamespace() ) )
{
- if($this->fld_subjectid)
+ if ( $this->fld_subjectid )
$getTitles[] = $t->getSubjectPage();
}
- else if($this->fld_talkid)
+ else if ( $this->fld_talkid )
$getTitles[] = $t->getTalkPage();
}
- if(!count($getTitles))
+ if ( !count( $getTitles ) )
return;
-
+
// Construct a custom WHERE clause that matches
// all titles in $getTitles
- $lb = new LinkBatch($getTitles);
+ $lb = new LinkBatch( $getTitles );
$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))
+ $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))
- $this->talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] =
- intval($row->page_id);
+ if ( MWNamespace::isTalk( $row->page_namespace ) )
+ $this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] =
+ intval( $row->page_id );
else
- $this->subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] =
- intval($row->page_id);
+ $this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] =
+ intval( $row->page_id );
+ }
+ }
+
+ /**
+ * Get information about watched status and put it in $this->watched
+ */
+ private function getWatchedInfo()
+ {
+ global $wgUser;
+
+ if ( $wgUser->isAnon() || count( $this->titles ) == 0 )
+ return;
+
+ $this->watched = array();
+ $db = $this->getDB();
+
+ $lb = new LinkBatch( $this->titles );
+
+ $this->resetQueryParams();
+ $this->addTables( array( 'page', 'watchlist' ) );
+ $this->addFields( array( 'page_title', 'page_namespace' ) );
+ $this->addWhere( array(
+ $lb->constructSet( 'page', $db ),
+ 'wl_namespace=page_namespace',
+ 'wl_title=page_title',
+ 'wl_user' => $wgUser->getID()
+ ) );
+
+ $res = $this->select( __METHOD__ );
+
+ while ( $row = $db->fetchObject( $res ) ) {
+ $this->watched[$row->page_namespace][$row->page_title] = true;
}
}
@@ -505,6 +559,7 @@ class ApiQueryInfo extends ApiQueryBase {
'talkid',
'subjectid',
'url',
+ 'preload',
);
if ( !is_null( $params['prop'] ) ) {
foreach ( $params['prop'] as $prop ) {
@@ -522,21 +577,23 @@ class ApiQueryInfo extends ApiQueryBase {
public function getAllowedParams() {
return array (
'prop' => array (
- ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_DFLT => null,
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
'protection',
'talkid',
+ 'watched', # private
'subjectid',
'url',
'readable', # private
+ 'preload'
// If you add more properties here, please consider whether they
// need to be added to getCacheMode()
- )),
+ ) ),
'token' => array (
- ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_DFLT => null,
ApiBase :: PARAM_ISMULTI => true,
- ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions())
+ ApiBase :: PARAM_TYPE => array_keys( $this->getTokenFunctions() )
),
'continue' => null,
);
@@ -548,7 +605,11 @@ class ApiQueryInfo extends ApiQueryBase {
'Which additional properties to get:',
' 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'
+ ' watched - List the watched status of each page',
+ ' subjectid - The page ID of the parent page for each talk page',
+ ' url - Gives a full URL to the page, and also an edit URL',
+ ' readable - Whether the user can read this page',
+ ' preload - Gives the text returned by EditFormPreloadText'
),
'token' => 'Request a token to perform a data-modifying action on a page',
'continue' => 'When more results are available, use this to continue',
@@ -558,6 +619,12 @@ class ApiQueryInfo extends ApiQueryBase {
public function getDescription() {
return 'Get basic page information such as namespace, title, last touched date, ...';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -567,6 +634,6 @@ class ApiQueryInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryInfo.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryInfo.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index 35f7e67c..9330e380 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryLangLinks extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'll');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'll' );
}
public function execute() {
@@ -44,52 +44,53 @@ class ApiQueryLangLinks extends ApiQueryBase {
return;
$params = $this->extractRequestParams();
- $this->addFields(array (
+ $this->addFields( array (
'll_from',
'll_lang',
'll_title'
- ));
+ ) );
- $this->addTables('langlinks');
- $this->addWhereFld('ll_from', array_keys($this->getPageSet()->getGoodTitles()));
- 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')");
+ $this->addTables( 'langlinks' );
+ $this->addWhereFld( 'll_from', array_keys( $this->getPageSet()->getGoodTitles() ) );
+ 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');
+
+ // 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__);
+ $this->addOption( 'ORDER BY', 'll_from, ll_lang' );
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $res = $this->select( __METHOD__ );
$count = 0;
$db = $this->getDB();
- while ($row = $db->fetchObject($res)) {
- if (++$count > $params['limit']) {
+ 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}");
+ $this->setContinueEnumParameter( 'continue', "{$row->ll_from}|{$row->ll_lang}" );
break;
}
- $entry = array('lang' => $row->ll_lang);
- ApiResult :: setContent($entry, $row->ll_title);
- $fit = $this->addPageSubItem($row->ll_from, $entry);
- if(!$fit)
+ $entry = array( 'lang' => $row->ll_lang );
+ ApiResult :: setContent( $entry, $row->ll_title );
+ $fit = $this->addPageSubItem( $row->ll_from, $entry );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue', "{$row->ll_from}|{$row->ll_lang}");
+ $this->setContinueEnumParameter( 'continue', "{$row->ll_from}|{$row->ll_lang}" );
break;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
public function getCacheMode( $params ) {
@@ -119,6 +120,12 @@ class ApiQueryLangLinks extends ApiQueryBase {
public function getDescription() {
return 'Returns all interlanguage links from the given page(s)';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -128,6 +135,6 @@ class ApiQueryLangLinks extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLangLinks.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryLangLinks.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php
index 94b7980c..52dfd591 100644
--- a/includes/api/ApiQueryLinks.php
+++ b/includes/api/ApiQueryLinks.php
@@ -23,9 +23,9 @@
* 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" );
}
/**
@@ -40,9 +40,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
private $table, $prefix, $description;
- public function __construct($query, $moduleName) {
+ public function __construct( $query, $moduleName ) {
- switch ($moduleName) {
+ switch ( $moduleName ) {
case self::LINKS :
$this->table = 'pagelinks';
$this->prefix = 'pl';
@@ -54,10 +54,10 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
$this->description = 'template';
break;
default :
- ApiBase :: dieDebug(__METHOD__, 'Unknown module name');
+ ApiBase :: dieDebug( __METHOD__, 'Unknown module name' );
}
- parent :: __construct($query, $moduleName, $this->prefix);
+ parent :: __construct( $query, $moduleName, $this->prefix );
}
public function execute() {
@@ -68,101 +68,101 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
return 'public';
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
- if ($this->getPageSet()->getGoodTitleCount() == 0)
+ if ( $this->getPageSet()->getGoodTitleCount() == 0 )
return; // nothing to do
$params = $this->extractRequestParams();
- $this->addFields(array (
+ $this->addFields( array (
$this->prefix . '_from AS pl_from',
$this->prefix . '_namespace AS pl_namespace',
$this->prefix . '_title AS pl_title'
- ));
-
- $this->addTables($this->table);
- $this->addWhereFld($this->prefix . '_from', array_keys($this->getPageSet()->getGoodTitles()));
- $this->addWhereFld($this->prefix . '_namespace', $params['namespace']);
-
- 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')))");
+ ) );
+
+ $this->addTables( $this->table );
+ $this->addWhereFld( $this->prefix . '_from', array_keys( $this->getPageSet()->getGoodTitles() ) );
+ $this->addWhereFld( $this->prefix . '_namespace', $params['namespace'] );
+
+ 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
+ // 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)
+ if ( count( $this->getPageSet()->getGoodTitles() ) != 1 )
$order[] = "{$this->prefix}_from";
- if(count($params['namespace']) != 1)
+ 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);
+ $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__);
+ $res = $this->select( __METHOD__ );
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if(++$count > $params['limit']) {
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('continue',
+ $this->setContinueEnumParameter( 'continue',
"{$row->pl_from}|{$row->pl_namespace}|" .
- $this->keyToTitle($row->pl_title));
+ $this->keyToTitle( $row->pl_title ) );
break;
}
$vals = array();
- ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->pl_namespace, $row->pl_title));
- $fit = $this->addPageSubItem($row->pl_from, $vals);
- if(!$fit)
+ ApiQueryBase :: addTitleInfo( $vals, Title :: makeTitle( $row->pl_namespace, $row->pl_title ) );
+ $fit = $this->addPageSubItem( $row->pl_from, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue',
+ $this->setContinueEnumParameter( 'continue',
"{$row->pl_from}|{$row->pl_namespace}|" .
- $this->keyToTitle($row->pl_title));
+ $this->keyToTitle( $row->pl_title ) );
break;
}
}
} else {
-
$titles = array();
$count = 0;
- while ($row = $db->fetchObject($res)) {
- if(++$count > $params['limit']) {
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( ++$count > $params['limit'] ) {
// We've reached the one extra which shows that
// there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('continue',
+ $this->setContinueEnumParameter( 'continue',
"{$row->pl_from}|{$row->pl_namespace}|" .
- $this->keyToTitle($row->pl_title));
+ $this->keyToTitle( $row->pl_title ) );
break;
}
- $titles[] = Title :: makeTitle($row->pl_namespace, $row->pl_title);
+ $titles[] = Title :: makeTitle( $row->pl_namespace, $row->pl_title );
}
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
public function getAllowedParams()
@@ -208,6 +208,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLinks.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryLinks.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 7afed844..bdeee952 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,136 +35,153 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryLogEvents extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'le');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'le' );
}
public function execute() {
$params = $this->extractRequestParams();
$db = $this->getDB();
-
- $prop = $params['prop'];
- $this->fld_ids = in_array('ids', $prop);
- $this->fld_title = in_array('title', $prop);
- $this->fld_type = in_array('type', $prop);
- $this->fld_user = in_array('user', $prop);
- $this->fld_timestamp = in_array('timestamp', $prop);
- $this->fld_comment = in_array('comment', $prop);
- $this->fld_details = in_array('details', $prop);
-
- list($tbl_logging, $tbl_page, $tbl_user) = $db->tableNamesN('logging', 'page', 'user');
-
- $hideLogs = LogEventsList::getExcludeClause($db);
- if($hideLogs !== false)
- $this->addWhere($hideLogs);
+
+ $prop = array_flip( $params['prop'] );
+
+ $this->fld_ids = isset( $prop['ids'] );
+ $this->fld_title = isset( $prop['title'] );
+ $this->fld_type = isset( $prop['type'] );
+ $this->fld_user = isset( $prop['user'] );
+ $this->fld_timestamp = isset( $prop['timestamp'] );
+ $this->fld_comment = isset( $prop['comment'] );
+ $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_details = isset( $prop['details'] );
+ $this->fld_tags = isset( $prop['tags'] );
+
+ list( $tbl_logging, $tbl_page, $tbl_user ) = $db->tableNamesN( 'logging', 'page', 'user' );
+
+ $hideLogs = LogEventsList::getExcludeClause( $db );
+ if ( $hideLogs !== false )
+ $this->addWhere( $hideLogs );
// Order is significant here
- $this->addTables(array('logging', 'user', 'page'));
- $this->addOption('STRAIGHT_JOIN');
- $this->addJoinConds(array(
- 'user' => array('JOIN',
- 'user_id=log_user'),
- 'page' => array('LEFT JOIN',
+ $this->addTables( array( 'logging', 'user', 'page' ) );
+ $this->addOption( 'STRAIGHT_JOIN' );
+ $this->addJoinConds( array(
+ 'user' => array( 'JOIN',
+ 'user_id=log_user' ),
+ 'page' => array( 'LEFT JOIN',
array( 'log_namespace=page_namespace',
- 'log_title=page_title'))));
- $index = 'times'; // default, may change
+ 'log_title=page_title' ) ) ) );
+ $index = array( 'logging' => 'times' ); // default, may change
- $this->addFields(array (
+ $this->addFields( array (
'log_type',
'log_action',
'log_timestamp',
'log_deleted',
- ));
-
- $this->addFieldsIf('log_id', $this->fld_ids);
- $this->addFieldsIf('page_id', $this->fld_ids);
- $this->addFieldsIf('log_user', $this->fld_user);
- $this->addFieldsIf('user_name', $this->fld_user);
- $this->addFieldsIf('log_namespace', $this->fld_title);
- $this->addFieldsIf('log_title', $this->fld_title);
- $this->addFieldsIf('log_comment', $this->fld_comment);
- $this->addFieldsIf('log_params', $this->fld_details);
+ ) );
+
+ $this->addFieldsIf( 'log_id', $this->fld_ids );
+ $this->addFieldsIf( 'page_id', $this->fld_ids );
+ $this->addFieldsIf( 'log_user', $this->fld_user );
+ $this->addFieldsIf( 'user_name', $this->fld_user );
+ $this->addFieldsIf( 'log_namespace', $this->fld_title );
+ $this->addFieldsIf( 'log_title', $this->fld_title );
+ $this->addFieldsIf( 'log_comment', $this->fld_comment || $this->fld_parsedcomment );
+ $this->addFieldsIf( 'log_params', $this->fld_details );
+
+ if ( $this->fld_tags ) {
+ $this->addTables( 'tag_summary' );
+ $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', 'log_id=ts_log_id' ) ) );
+ $this->addFields( 'ts_tags' );
+ }
+
+ if ( !is_null( $params['tag'] ) ) {
+ $this->addTables( 'change_tag' );
+ $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'log_id=ct_log_id' ) ) ) );
+ $this->addWhereFld( 'ct_tag', $params['tag'] );
+ global $wgOldChangeTagsIndex;
+ $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
+ }
- if( !is_null($params['type']) ) {
- $this->addWhereFld('log_type', $params['type']);
- $index = 'type_time';
+ if ( !is_null( $params['type'] ) ) {
+ $this->addWhereFld( 'log_type', $params['type'] );
+ $index['logging'] = 'type_time';
}
- $this->addWhereRange('log_timestamp', $params['dir'], $params['start'], $params['end']);
+ $this->addWhereRange( 'log_timestamp', $params['dir'], $params['start'], $params['end'] );
$limit = $params['limit'];
- $this->addOption('LIMIT', $limit +1);
+ $this->addOption( 'LIMIT', $limit + 1 );
$user = $params['user'];
- if (!is_null($user)) {
- $userid = User::idFromName($user);
- if (!$userid)
- $this->dieUsage("User name $user not found", 'param_user');
- $this->addWhereFld('log_user', $userid);
- $index = 'user_time';
+ if ( !is_null( $user ) ) {
+ $userid = User::idFromName( $user );
+ if ( !$userid )
+ $this->dieUsage( "User name $user not found", 'param_user' );
+ $this->addWhereFld( 'log_user', $userid );
+ $index['logging'] = 'user_time';
}
$title = $params['title'];
- if (!is_null($title)) {
- $titleObj = Title :: newFromText($title);
- if (is_null($titleObj))
- $this->dieUsage("Bad title value '$title'", 'param_title');
- $this->addWhereFld('log_namespace', $titleObj->getNamespace());
- $this->addWhereFld('log_title', $titleObj->getDBkey());
+ if ( !is_null( $title ) ) {
+ $titleObj = Title :: newFromText( $title );
+ if ( is_null( $titleObj ) )
+ $this->dieUsage( "Bad title value '$title'", 'param_title' );
+ $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() );
+ $this->addWhereFld( 'log_title', $titleObj->getDBkey() );
// Use the title index in preference to the user index if there is a conflict
- $index = is_null($user) ? 'page_time' : array('page_time','user_time');
+ $index['logging'] = is_null( $user ) ? 'page_time' : array( 'page_time', 'user_time' );
}
- $this->addOption( 'USE INDEX', array( 'logging' => $index ) );
+ $this->addOption( 'USE INDEX', $index );
// Paranoia: avoid brute force searches (bug 17342)
- if (!is_null($title)) {
- $this->addWhere('log_deleted & ' . LogPage::DELETED_ACTION . ' = 0');
+ if ( !is_null( $title ) ) {
+ $this->addWhere( $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0' );
}
- if (!is_null($user)) {
- $this->addWhere('log_deleted & ' . LogPage::DELETED_USER . ' = 0');
+ if ( !is_null( $user ) ) {
+ $this->addWhere( $db->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0' );
}
$count = 0;
- $res = $this->select(__METHOD__);
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $limit) {
+ $res = $this->select( __METHOD__ );
+ while ( $row = $db->fetchObject( $res ) ) {
+ if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->log_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->log_timestamp ) );
break;
}
- $vals = $this->extractRowInfo($row);
- if(!$vals)
+ $vals = $this->extractRowInfo( $row );
+ if ( !$vals )
continue;
- $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->log_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->log_timestamp ) );
break;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item');
+ $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' );
}
- public static function addLogParams($result, &$vals, $params, $type, $ts) {
- $params = explode("\n", $params);
- switch ($type) {
+ public static function addLogParams( $result, &$vals, $params, $type, $ts ) {
+ $params = explode( "\n", $params );
+ switch ( $type ) {
case 'move':
- if (isset ($params[0])) {
- $title = Title :: newFromText($params[0]);
- if ($title) {
+ if ( isset ( $params[0] ) ) {
+ $title = Title :: newFromText( $params[0] );
+ if ( $title ) {
$vals2 = array();
- ApiQueryBase :: addTitleInfo($vals2, $title, "new_");
+ ApiQueryBase :: addTitleInfo( $vals2, $title, "new_" );
$vals[$type] = $vals2;
}
}
- if (isset ($params[1]) && $params[1]) {
+ if ( isset ( $params[1] ) && $params[1] ) {
$vals[$type]['suppressedredirect'] = '';
- }
+ }
$params = null;
break;
case 'patrol':
@@ -182,77 +199,99 @@ class ApiQueryLogEvents extends ApiQueryBase {
case 'block':
$vals2 = array();
list( $vals2['duration'], $vals2['flags'] ) = $params;
- $vals2['expiry'] = wfTimestamp(TS_ISO_8601,
- strtotime($params[0], wfTimestamp(TS_UNIX, $ts)));
+ $vals2['expiry'] = wfTimestamp( TS_ISO_8601,
+ strtotime( $params[0], wfTimestamp( TS_UNIX, $ts ) ) );
$vals[$type] = $vals2;
$params = null;
break;
}
- if (!is_null($params)) {
- $result->setIndexedTagName($params, 'param');
- $vals = array_merge($vals, $params);
+ if ( !is_null( $params ) ) {
+ $result->setIndexedTagName( $params, 'param' );
+ $vals = array_merge( $vals, $params );
}
return $vals;
}
- private function extractRowInfo($row) {
+ private function extractRowInfo( $row ) {
$vals = array();
- if ($this->fld_ids) {
- $vals['logid'] = intval($row->log_id);
- $vals['pageid'] = intval($row->page_id);
+ if ( $this->fld_ids ) {
+ $vals['logid'] = intval( $row->log_id );
+ $vals['pageid'] = intval( $row->page_id );
}
- if ($this->fld_title) {
- if (LogEventsList::isDeleted($row, LogPage::DELETED_ACTION)) {
+ $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+
+ if ( $this->fld_title ) {
+ if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
$vals['actionhidden'] = '';
} else {
- $title = Title :: makeTitle($row->log_namespace, $row->log_title);
- ApiQueryBase :: addTitleInfo($vals, $title);
+ ApiQueryBase::addTitleInfo( $vals, $title );
}
}
- if ($this->fld_type) {
+ if ( $this->fld_type ) {
$vals['type'] = $row->log_type;
$vals['action'] = $row->log_action;
}
- if ($this->fld_details && $row->log_params !== '') {
- if (LogEventsList::isDeleted($row, LogPage::DELETED_ACTION)) {
+ if ( $this->fld_details && $row->log_params !== '' ) {
+ if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
$vals['actionhidden'] = '';
} else {
- self::addLogParams($this->getResult(), $vals,
+ self::addLogParams( $this->getResult(), $vals,
$row->log_params, $row->log_type,
- $row->log_timestamp);
+ $row->log_timestamp );
}
}
- if ($this->fld_user) {
- if (LogEventsList::isDeleted($row, LogPage::DELETED_USER)) {
+ if ( $this->fld_user ) {
+ if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
$vals['userhidden'] = '';
} else {
$vals['user'] = $row->user_name;
- if(!$row->log_user)
+ if ( !$row->log_user )
$vals['anon'] = '';
}
}
- if ($this->fld_timestamp) {
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->log_timestamp);
+ if ( $this->fld_timestamp ) {
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->log_timestamp );
}
- if ($this->fld_comment && isset($row->log_comment)) {
- if (LogEventsList::isDeleted($row, LogPage::DELETED_COMMENT)) {
+
+ if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->log_comment ) ) {
+ if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
$vals['commenthidden'] = '';
} else {
- $vals['comment'] = $row->log_comment;
+ if ( $this->fld_comment )
+ $vals['comment'] = $row->log_comment;
+
+ if ( $this->fld_parsedcomment ) {
+ global $wgUser;
+ $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->log_comment, $title );
+ }
}
}
+ if ( $this->fld_tags ) {
+ if ( $row->ts_tags ) {
+ $tags = explode( ',', $row->ts_tags );
+ $this->getResult()->setIndexedTagName( $tags, 'tag' );
+ $vals['tags'] = $tags;
+ } else {
+ $vals['tags'] = array();
+ }
+ }
+
return $vals;
}
-
-
+
public function getCacheMode( $params ) {
- return 'public';
+ if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
+ // formatComment() calls wfMsg() among other things
+ return 'anon-public-user-private';
+ } else {
+ return 'public';
+ }
}
public function getAllowedParams() {
@@ -268,7 +307,9 @@ class ApiQueryLogEvents extends ApiQueryBase {
'user',
'timestamp',
'comment',
+ 'parsedcomment',
'details',
+ 'tags'
)
),
'type' => array (
@@ -289,6 +330,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
),
'user' => null,
'title' => null,
+ 'tag' => null,
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
@@ -308,13 +350,21 @@ class ApiQueryLogEvents extends ApiQueryBase {
'dir' => 'In which direction to enumerate.',
'user' => 'Filter entries to those made by the given user.',
'title' => 'Filter entries to those related to a page.',
- 'limit' => 'How many total event entries to return.'
+ 'limit' => 'How many total event entries to return.',
+ 'tag' => 'Only list event entries tagged with this tag.',
);
}
public function getDescription() {
return 'Get events from logs.';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'param_user', 'info' => 'User name $user not found' ),
+ array( 'code' => 'param_title', 'info' => 'Bad title value \'title\'' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -323,6 +373,6 @@ class ApiQueryLogEvents extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryLogEvents.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryLogEvents.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php
index 67a2a829..ab794805 100644
--- a/includes/api/ApiQueryProtectedTitles.php
+++ b/includes/api/ApiQueryProtectedTitles.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,91 +35,104 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'pt');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'pt' );
}
public function execute() {
$this->run();
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
$db = $this->getDB();
$params = $this->extractRequestParams();
- $this->addTables('protected_titles');
- $this->addFields(array('pt_namespace', 'pt_title', 'pt_timestamp'));
+ $this->addTables( 'protected_titles' );
+ $this->addFields( array( 'pt_namespace', 'pt_title', 'pt_timestamp' ) );
- $prop = array_flip($params['prop']);
- $this->addFieldsIf('pt_user', isset($prop['user']));
- $this->addFieldsIf('pt_reason', isset($prop['comment']));
- $this->addFieldsIf('pt_expiry', isset($prop['expiry']));
- $this->addFieldsIf('pt_create_perm', isset($prop['level']));
+ $prop = array_flip( $params['prop'] );
+ $this->addFieldsIf( 'pt_user', isset( $prop['user'] ) );
+ $this->addFieldsIf( 'pt_reason', isset( $prop['comment'] ) || isset( $prop['parsedcomment'] ) );
+ $this->addFieldsIf( 'pt_expiry', isset( $prop['expiry'] ) );
+ $this->addFieldsIf( 'pt_create_perm', isset( $prop['level'] ) );
- $this->addWhereRange('pt_timestamp', $params['dir'], $params['start'], $params['end']);
- $this->addWhereFld('pt_namespace', $params['namespace']);
- $this->addWhereFld('pt_create_perm', $params['level']);
+ $this->addWhereRange( 'pt_timestamp', $params['dir'], $params['start'], $params['end'] );
+ $this->addWhereFld( 'pt_namespace', $params['namespace'] );
+ $this->addWhereFld( 'pt_create_perm', $params['level'] );
- if(isset($prop['user']))
+ if ( isset( $prop['user'] ) )
{
- $this->addTables('user');
- $this->addFields('user_name');
- $this->addJoinConds(array('user' => array('LEFT JOIN',
+ $this->addTables( 'user' );
+ $this->addFields( 'user_name' );
+ $this->addJoinConds( array( 'user' => array( 'LEFT JOIN',
'user_id=pt_user'
- )));
+ ) ) );
}
- $this->addOption('LIMIT', $params['limit'] + 1);
- $res = $this->select(__METHOD__);
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $res = $this->select( __METHOD__ );
$count = 0;
$result = $this->getResult();
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $params['limit']) {
+ 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('start', wfTimestamp(TS_ISO_8601, $row->pt_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->pt_timestamp ) );
break;
}
- $title = Title::makeTitle($row->pt_namespace, $row->pt_title);
- if (is_null($resultPageSet)) {
+ $title = Title::makeTitle( $row->pt_namespace, $row->pt_title );
+ if ( is_null( $resultPageSet ) ) {
$vals = array();
- ApiQueryBase::addTitleInfo($vals, $title);
- if(isset($prop['timestamp']))
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->pt_timestamp);
- if(isset($prop['user']) && !is_null($row->user_name))
+ ApiQueryBase::addTitleInfo( $vals, $title );
+ if ( isset( $prop['timestamp'] ) )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->pt_timestamp );
+
+ if ( isset( $prop['user'] ) && !is_null( $row->user_name ) )
$vals['user'] = $row->user_name;
- if(isset($prop['comment']))
+
+ if ( isset( $prop['comment'] ) )
$vals['comment'] = $row->pt_reason;
- if(isset($prop['expiry']))
- $vals['expiry'] = Block::decodeExpiry($row->pt_expiry, TS_ISO_8601);
- if(isset($prop['level']))
+
+ if ( isset( $prop['parsedcomment'] ) ) {
+ global $wgUser;
+ $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->pt_reason, $title );
+ }
+
+ if ( isset( $prop['expiry'] ) )
+ $vals['expiry'] = Block::decodeExpiry( $row->pt_expiry, TS_ISO_8601 );
+
+ if ( isset( $prop['level'] ) )
$vals['level'] = $row->pt_create_perm;
- $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
- {
- $this->setContinueEnumParameter('start',
- wfTimestamp(TS_ISO_8601, $row->pt_timestamp));
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'start',
+ wfTimestamp( TS_ISO_8601, $row->pt_timestamp ) );
break;
}
} else {
$titles[] = $title;
}
}
- $db->freeResult($res);
- if(is_null($resultPageSet))
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), $this->getModulePrefix());
+ $db->freeResult( $res );
+ if ( is_null( $resultPageSet ) )
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $this->getModulePrefix() );
else
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
public function getCacheMode( $params ) {
- return 'public';
+ if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
+ // formatComment() calls wfMsg() among other things
+ return 'anon-public-user-private';
+ } else {
+ return 'public';
+ }
}
public function getAllowedParams() {
@@ -131,7 +144,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
),
'level' => array(
ApiBase :: PARAM_ISMULTI => true,
- ApiBase :: PARAM_TYPE => array_diff($wgRestrictionLevels, array(''))
+ ApiBase :: PARAM_TYPE => array_diff( $wgRestrictionLevels, array( '' ) )
),
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
@@ -160,6 +173,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
'timestamp',
'user',
'comment',
+ 'parsedcomment',
'expiry',
'level'
)
@@ -190,6 +204,6 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryProtectedTitles.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryProtectedTitles.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
index 1811b7e8..10796810 100644
--- a/includes/api/ApiQueryRandom.php
+++ b/includes/api/ApiQueryRandom.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -36,88 +36,88 @@ if (!defined('MEDIAWIKI')) {
class ApiQueryRandom extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'rn');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'rn' );
}
public function execute() {
$this->run();
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- protected function prepareQuery($randstr, $limit, $namespace, &$resultPageSet, $redirect) {
+ protected function prepareQuery( $randstr, $limit, $namespace, &$resultPageSet, $redirect ) {
$this->resetQueryParams();
- $this->addTables('page');
- $this->addOption('LIMIT', $limit);
- $this->addWhereFld('page_namespace', $namespace);
- $this->addWhereRange('page_random', 'newer', $randstr, null);
- $this->addWhereFld('page_is_redirect', $redirect);
- $this->addOption('USE INDEX', 'page_random');
- if(is_null($resultPageSet))
- $this->addFields(array('page_id', 'page_title', 'page_namespace'));
+ $this->addTables( 'page' );
+ $this->addOption( 'LIMIT', $limit );
+ $this->addWhereFld( 'page_namespace', $namespace );
+ $this->addWhereRange( 'page_random', 'newer', $randstr, null );
+ $this->addWhereFld( 'page_is_redirect', $redirect );
+ $this->addOption( 'USE INDEX', 'page_random' );
+ if ( is_null( $resultPageSet ) )
+ $this->addFields( array( 'page_id', 'page_title', 'page_namespace' ) );
else
- $this->addFields($resultPageSet->getPageTableFields());
+ $this->addFields( $resultPageSet->getPageTableFields() );
}
- protected function runQuery(&$resultPageSet) {
+ protected function runQuery( &$resultPageSet ) {
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
$count = 0;
- while($row = $db->fetchObject($res)) {
+ while ( $row = $db->fetchObject( $res ) ) {
$count++;
- if(is_null($resultPageSet))
+ if ( is_null( $resultPageSet ) )
{
// Prevent duplicates
- if(!in_array($row->page_id, $this->pageIDs))
+ if ( !in_array( $row->page_id, $this->pageIDs ) )
{
$fit = $this->getResult()->addValue(
- array('query', $this->getModuleName()),
- null, $this->extractRowInfo($row));
- if(!$fit)
- # We can't really query-continue a random list.
- # Return an insanely high value so
- # $count < $limit is false
+ array( 'query', $this->getModuleName() ),
+ null, $this->extractRowInfo( $row ) );
+ if ( !$fit )
+ // We can't really query-continue a random list.
+ // Return an insanely high value so
+ // $count < $limit is false
return 1E9;
$this->pageIDs[] = $row->page_id;
}
}
else
- $resultPageSet->processDbRow($row);
+ $resultPageSet->processDbRow( $row );
}
- $db->freeResult($res);
+ $db->freeResult( $res );
return $count;
}
- public function run($resultPageSet = null) {
+ public function run( $resultPageSet = null ) {
$params = $this->extractRequestParams();
$result = $this->getResult();
$this->pageIDs = array();
- $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet, $params['redirect']);
- $count = $this->runQuery($resultPageSet);
- if($count < $params['limit'])
+ $this->prepareQuery( wfRandom(), $params['limit'], $params['namespace'], $resultPageSet, $params['redirect'] );
+ $count = $this->runQuery( $resultPageSet );
+ if ( $count < $params['limit'] )
{
/* We got too few pages, we probably picked a high value
* for page_random. We'll just take the lowest ones, see
* also the comment in Title::getRandomTitle()
*/
- $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet, $params['redirect']);
- $this->runQuery($resultPageSet);
+ $this->prepareQuery( 0, $params['limit'] - $count, $params['namespace'], $resultPageSet, $params['redirect'] );
+ $this->runQuery( $resultPageSet );
}
- if(is_null($resultPageSet)) {
- $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'page');
+ if ( is_null( $resultPageSet ) ) {
+ $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' );
}
}
- private function extractRowInfo($row) {
- $title = Title::makeTitle($row->page_namespace, $row->page_title);
+ private function extractRowInfo( $row ) {
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
$vals = array();
- $vals['id'] = intval($row->page_id);
- ApiQueryBase::addTitleInfo($vals, $title);
+ $vals['id'] = intval( $row->page_id );
+ ApiQueryBase::addTitleInfo( $vals, $title );
return $vals;
}
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index b5a56864..1f0de3be 100644
--- a/includes/api/ApiQueryRecentChanges.php
+++ b/includes/api/ApiQueryRecentChanges.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -36,43 +36,45 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryRecentChanges extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'rc');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'rc' );
}
- private $fld_comment = false, $fld_user = false, $fld_flags = false,
+ private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_flags = false,
$fld_timestamp = false, $fld_title = false, $fld_ids = false,
$fld_sizes = false;
-
+ /**
+ * Get an array mapping token names to their handler functions.
+ * The prototype for a token function is func($pageid, $title, $rc)
+ * it should return a token or false (permission denied)
+ * @return array(tokenname => function)
+ */
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))
+ 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')))
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) )
return array();
$this->tokenFunctions = array(
'patrol' => array( 'ApiQueryRecentChanges', 'getPatrolToken' )
);
- wfRunHooks('APIQueryRecentChangesTokens', array(&$this->tokenFunctions));
+ wfRunHooks( 'APIQueryRecentChangesTokens', array( &$this->tokenFunctions ) );
return $this->tokenFunctions;
}
- public static function getPatrolToken($pageid, $title, $rc)
+ public static function getPatrolToken( $pageid, $title, $rc )
{
global $wgUser;
- if(!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
+ if ( !$wgUser->useRCPatrol() && ( !$wgUser->useNPPatrol() ||
+ $rc->getAttribute( 'rc_type' ) != RC_NEW ) )
return false;
// The patrol token is always the same, let's exploit that
static $cachedPatrolToken = null;
- if(!is_null($cachedPatrolToken))
+ if ( !is_null( $cachedPatrolToken ) )
return $cachedPatrolToken;
$cachedPatrolToken = $wgUser->editToken();
@@ -80,6 +82,25 @@ class ApiQueryRecentChanges extends ApiQueryBase {
}
/**
+ * Sets internal state to include the desired properties in the output.
+ * @param $prop associative array of properties, only keys are used here
+ */
+ public function initProperties( $prop ) {
+ $this->fld_comment = isset ( $prop['comment'] );
+ $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_user = isset ( $prop['user'] );
+ $this->fld_flags = isset ( $prop['flags'] );
+ $this->fld_timestamp = isset ( $prop['timestamp'] );
+ $this->fld_title = isset ( $prop['title'] );
+ $this->fld_ids = isset ( $prop['ids'] );
+ $this->fld_sizes = isset ( $prop['sizes'] );
+ $this->fld_redirect = isset( $prop['redirect'] );
+ $this->fld_patrolled = isset( $prop['patrolled'] );
+ $this->fld_loginfo = isset( $prop['loginfo'] );
+ $this->fld_tags = isset( $prop['tags'] );
+ }
+
+ /**
* Generates and outputs the result of this query based upon the provided parameters.
*/
public function execute() {
@@ -92,49 +113,65 @@ class ApiQueryRecentChanges extends ApiQueryBase {
* AND rc_deleted = '0'
*/
$db = $this->getDB();
- $this->addTables('recentchanges');
- $this->addOption('USE INDEX', array('recentchanges' => 'rc_timestamp'));
- $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']);
- $this->addWhereFld('rc_namespace', $params['namespace']);
- $this->addWhereFld('rc_deleted', 0);
+ $this->addTables( 'recentchanges' );
+ $index = array( 'recentchanges' => 'rc_timestamp' ); // May change
+ $this->addWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] );
+ $this->addWhereFld( 'rc_namespace', $params['namespace'] );
+ $this->addWhereFld( 'rc_deleted', 0 );
- if(!is_null($params['type']))
- $this->addWhereFld('rc_type', $this->parseRCType($params['type']));
+ if ( !is_null( $params['type'] ) )
+ $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) );
- if (!is_null($params['show'])) {
- $show = array_flip($params['show']);
+ if ( !is_null( $params['show'] ) ) {
+ $show = array_flip( $params['show'] );
/* Check for conflicting parameters. */
- if ((isset ($show['minor']) && isset ($show['!minor']))
- || (isset ($show['bot']) && isset ($show['!bot']))
- || (isset ($show['anon']) && isset ($show['!anon']))
- || (isset ($show['redirect']) && isset ($show['!redirect']))
- || (isset ($show['patrolled']) && isset ($show['!patrolled']))) {
+ 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');
+ $this->dieUsageMsg( array( 'show' ) );
}
// Check permissions
global $wgUser;
- if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
- $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
+ if ( ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol() )
+ $this->dieUsage( "You need the patrol right to request the patrolled flag", 'permissiondenied' );
/* Add additional conditions to query depending upon parameters. */
- $this->addWhereIf('rc_minor = 0', isset ($show['!minor']));
- $this->addWhereIf('rc_minor != 0', isset ($show['minor']));
- $this->addWhereIf('rc_bot = 0', isset ($show['!bot']));
- $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']));
+ $this->addWhereIf( 'rc_minor = 0', isset ( $show['!minor'] ) );
+ $this->addWhereIf( 'rc_minor != 0', isset ( $show['minor'] ) );
+ $this->addWhereIf( 'rc_bot = 0', isset ( $show['!bot'] ) );
+ $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']));
+ $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. */
- $this->addFields(array (
+
+ if ( !is_null( $params['user'] ) && !is_null( $param['excludeuser'] ) )
+ $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
+
+ if ( !is_null( $params['user'] ) )
+ {
+ $this->addWhereFld( 'rc_user_text', $params['user'] );
+ $index['recentchanges'] = 'rc_user_text';
+ }
+
+ if ( !is_null( $params['excludeuser'] ) )
+ // We don't use the rc_user_text index here because
+ // * it would require us to sort by rc_user_text before rc_timestamp
+ // * the != condition doesn't throw out too many rows anyway
+ $this->addWhere( 'rc_user_text != ' . $this->getDB()->addQuotes( $params['excludeuser'] ) );
+
+ /* Add the fields we're concerned with to our query. */
+ $this->addFields( array (
'rc_timestamp',
'rc_namespace',
'rc_title',
@@ -142,86 +179,93 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'rc_type',
'rc_moved_to_ns',
'rc_moved_to_title'
- ));
+ ) );
/* Determine what properties we need to display. */
- if (!is_null($params['prop'])) {
- $prop = array_flip($params['prop']);
+ if ( !is_null( $params['prop'] ) ) {
+ $prop = array_flip( $params['prop'] );
/* Set up internal members based upon params. */
- $this->fld_comment = isset ($prop['comment']);
- $this->fld_user = isset ($prop['user']);
- $this->fld_flags = isset ($prop['flags']);
- $this->fld_timestamp = isset ($prop['timestamp']);
- $this->fld_title = isset ($prop['title']);
- $this->fld_ids = isset ($prop['ids']);
- $this->fld_sizes = isset ($prop['sizes']);
- $this->fld_redirect = isset($prop['redirect']);
- $this->fld_patrolled = isset($prop['patrolled']);
- $this->fld_loginfo = isset($prop['loginfo']);
+ $this->initProperties( $prop );
global $wgUser;
- if($this->fld_patrolled && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
- $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
+ if ( $this->fld_patrolled && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol() )
+ $this->dieUsage( "You need the patrol right to request the patrolled flag", 'permissiondenied' );
/* Add fields to our query if they are specified as a needed parameter. */
- $this->addFieldsIf('rc_id', $this->fld_ids);
- $this->addFieldsIf('rc_this_oldid', $this->fld_ids);
- $this->addFieldsIf('rc_last_oldid', $this->fld_ids);
- $this->addFieldsIf('rc_comment', $this->fld_comment);
- $this->addFieldsIf('rc_user', $this->fld_user);
- $this->addFieldsIf('rc_user_text', $this->fld_user);
- $this->addFieldsIf('rc_minor', $this->fld_flags);
- $this->addFieldsIf('rc_bot', $this->fld_flags);
- $this->addFieldsIf('rc_new', $this->fld_flags);
- $this->addFieldsIf('rc_old_len', $this->fld_sizes);
- $this->addFieldsIf('rc_new_len', $this->fld_sizes);
- $this->addFieldsIf('rc_patrolled', $this->fld_patrolled);
- $this->addFieldsIf('rc_logid', $this->fld_loginfo);
- $this->addFieldsIf('rc_log_type', $this->fld_loginfo);
- $this->addFieldsIf('rc_log_action', $this->fld_loginfo);
- $this->addFieldsIf('rc_params', $this->fld_loginfo);
- if($this->fld_redirect || isset($show['redirect']) || isset($show['!redirect']))
+ $this->addFieldsIf( 'rc_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->fld_parsedcomment );
+ $this->addFieldsIf( 'rc_user', $this->fld_user );
+ $this->addFieldsIf( 'rc_user_text', $this->fld_user );
+ $this->addFieldsIf( 'rc_minor', $this->fld_flags );
+ $this->addFieldsIf( 'rc_bot', $this->fld_flags );
+ $this->addFieldsIf( 'rc_new', $this->fld_flags );
+ $this->addFieldsIf( 'rc_old_len', $this->fld_sizes );
+ $this->addFieldsIf( 'rc_new_len', $this->fld_sizes );
+ $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
+ $this->addFieldsIf( 'rc_logid', $this->fld_loginfo );
+ $this->addFieldsIf( 'rc_log_type', $this->fld_loginfo );
+ $this->addFieldsIf( 'rc_log_action', $this->fld_loginfo );
+ $this->addFieldsIf( 'rc_params', $this->fld_loginfo );
+ if ( $this->fld_redirect || isset( $show['redirect'] ) || isset( $show['!redirect'] ) )
{
- $this->addTables('page');
- $this->addJoinConds(array('page' => array('LEFT JOIN', array('rc_namespace=page_namespace', 'rc_title=page_title'))));
- $this->addFields('page_is_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' );
}
}
+
+ if ( $this->fld_tags ) {
+ $this->addTables( 'tag_summary' );
+ $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rc_id=ts_rc_id' ) ) ) );
+ $this->addFields( 'ts_tags' );
+ }
+
+ if ( !is_null( $params['tag'] ) ) {
+ $this->addTables( 'change_tag' );
+ $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rc_id=ct_rc_id' ) ) ) );
+ $this->addWhereFld( 'ct_tag' , $params['tag'] );
+ global $wgOldChangeTagsIndex;
+ $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
+ }
+
$this->token = $params['token'];
- $this->addOption('LIMIT', $params['limit'] +1);
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $this->addOption( 'USE INDEX', $index );
$count = 0;
/* Perform the actual query. */
$db = $this->getDB();
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
/* Iterate through the rows, adding data extracted from them to our query result. */
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $params['limit']) {
+ 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('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
break;
}
/* Extract the data from a single row. */
- $vals = $this->extractRowInfo($row);
+ $vals = $this->extractRowInfo( $row );
/* Add that row's data to our final output. */
- if(!$vals)
+ if ( !$vals )
continue;
- $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
break;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
/* Format the result */
- $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'rc');
+ $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'rc' );
}
/**
@@ -229,16 +273,16 @@ class ApiQueryRecentChanges extends ApiQueryBase {
*
* @param $row The row from which to extract the data.
* @return An array mapping strings (descriptors) to their respective string values.
- * @access private
+ * @access public
*/
- private function extractRowInfo($row) {
+ public function extractRowInfo( $row ) {
/* If page was moved somewhere, get the title of the move target. */
$movedToTitle = false;
- if (isset($row->rc_moved_to_title) && $row->rc_moved_to_title !== '')
- $movedToTitle = Title :: makeTitle($row->rc_moved_to_ns, $row->rc_moved_to_title);
+ if ( isset( $row->rc_moved_to_title ) && $row->rc_moved_to_title !== '' )
+ $movedToTitle = Title :: makeTitle( $row->rc_moved_to_ns, $row->rc_moved_to_title );
/* Determine the title of the page that has been changed. */
- $title = Title :: makeTitle($row->rc_namespace, $row->rc_title);
+ $title = Title :: makeTitle( $row->rc_namespace, $row->rc_title );
/* Our output data. */
$vals = array ();
@@ -247,87 +291,112 @@ class ApiQueryRecentChanges extends ApiQueryBase {
/* Determine what kind of change this was. */
switch ( $type ) {
- case RC_EDIT: $vals['type'] = 'edit'; break;
- case RC_NEW: $vals['type'] = 'new'; break;
- case RC_MOVE: $vals['type'] = 'move'; break;
- case RC_LOG: $vals['type'] = 'log'; break;
- case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break;
- default: $vals['type'] = $type;
+ case RC_EDIT:
+ $vals['type'] = 'edit';
+ break;
+ case RC_NEW:
+ $vals['type'] = 'new';
+ break;
+ case RC_MOVE:
+ $vals['type'] = 'move';
+ break;
+ case RC_LOG:
+ $vals['type'] = 'log';
+ break;
+ case RC_MOVE_OVER_REDIRECT:
+ $vals['type'] = 'move over redirect';
+ break;
+ default:
+ $vals['type'] = $type;
}
/* Create a new entry in the result for the title. */
- if ($this->fld_title) {
- ApiQueryBase :: addTitleInfo($vals, $title);
- if ($movedToTitle)
- ApiQueryBase :: addTitleInfo($vals, $movedToTitle, "new_");
+ if ( $this->fld_title ) {
+ ApiQueryBase :: addTitleInfo( $vals, $title );
+ if ( $movedToTitle )
+ ApiQueryBase :: addTitleInfo( $vals, $movedToTitle, "new_" );
}
/* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */
- if ($this->fld_ids) {
- $vals['rcid'] = intval($row->rc_id);
- $vals['pageid'] = intval($row->rc_cur_id);
- $vals['revid'] = intval($row->rc_this_oldid);
+ if ( $this->fld_ids ) {
+ $vals['rcid'] = intval( $row->rc_id );
+ $vals['pageid'] = intval( $row->rc_cur_id );
+ $vals['revid'] = intval( $row->rc_this_oldid );
$vals['old_revid'] = intval( $row->rc_last_oldid );
}
/* Add user data and 'anon' flag, if use is anonymous. */
- if ($this->fld_user) {
+ if ( $this->fld_user ) {
$vals['user'] = $row->rc_user_text;
- if(!$row->rc_user)
+ if ( !$row->rc_user )
$vals['anon'] = '';
}
/* Add flags, such as new, minor, bot. */
- if ($this->fld_flags) {
- if ($row->rc_bot)
+ if ( $this->fld_flags ) {
+ if ( $row->rc_bot )
$vals['bot'] = '';
- if ($row->rc_new)
+ if ( $row->rc_new )
$vals['new'] = '';
- if ($row->rc_minor)
+ if ( $row->rc_minor )
$vals['minor'] = '';
}
/* Add sizes of each revision. (Only available on 1.10+) */
- if ($this->fld_sizes) {
- $vals['oldlen'] = intval($row->rc_old_len);
- $vals['newlen'] = intval($row->rc_new_len);
+ if ( $this->fld_sizes ) {
+ $vals['oldlen'] = intval( $row->rc_old_len );
+ $vals['newlen'] = intval( $row->rc_new_len );
}
/* Add the timestamp. */
- if ($this->fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp);
+ if ( $this->fld_timestamp )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
/* Add edit summary / log summary. */
- if ($this->fld_comment && isset($row->rc_comment)) {
+ if ( $this->fld_comment && isset( $row->rc_comment ) )
$vals['comment'] = $row->rc_comment;
+
+ if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
+ global $wgUser;
+ $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->rc_comment, $title );
}
- if ($this->fld_redirect)
- if($row->page_is_redirect)
+ if ( $this->fld_redirect )
+ if ( $row->page_is_redirect )
$vals['redirect'] = '';
/* Add the patrolled flag */
- if ($this->fld_patrolled && $row->rc_patrolled == 1)
+ if ( $this->fld_patrolled && $row->rc_patrolled == 1 )
$vals['patrolled'] = '';
- if ($this->fld_loginfo && $row->rc_type == RC_LOG) {
- $vals['logid'] = intval($row->rc_logid);
+ if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
+ $vals['logid'] = intval( $row->rc_logid );
$vals['logtype'] = $row->rc_log_type;
$vals['logaction'] = $row->rc_log_action;
- ApiQueryLogEvents::addLogParams($this->getResult(),
+ ApiQueryLogEvents::addLogParams( $this->getResult(),
$vals, $row->rc_params,
- $row->rc_log_type, $row->rc_timestamp);
+ $row->rc_log_type, $row->rc_timestamp );
}
- if(!is_null($this->token))
+ if ( $this->fld_tags ) {
+ if ( $row->ts_tags ) {
+ $tags = explode( ',', $row->ts_tags );
+ $this->getResult()->setIndexedTagName( $tags, 'tag' );
+ $vals['tags'] = $tags;
+ } else {
+ $vals['tags'] = array();
+ }
+ }
+
+ if ( !is_null( $this->token ) )
{
$tokenFunctions = $this->getTokenFunctions();
- foreach($this->token as $t)
+ foreach ( $this->token as $t )
{
- $val = call_user_func($tokenFunctions[$t], $row->rc_cur_id,
- $title, RecentChange::newFromRow($row));
- if($val === false)
- $this->setWarning("Action '$t' is not allowed for the current user");
+ $val = call_user_func( $tokenFunctions[$t], $row->rc_cur_id,
+ $title, RecentChange::newFromRow( $row ) );
+ if ( $val === false )
+ $this->setWarning( "Action '$t' is not allowed for the current user" );
else
$vals[$t . 'token'] = $val;
}
@@ -336,16 +405,16 @@ class ApiQueryRecentChanges extends ApiQueryBase {
return $vals;
}
- private function parseRCType($type)
+ private function parseRCType( $type )
{
- if(is_array($type))
+ if ( is_array( $type ) )
{
$retval = array();
- foreach($type as $t)
- $retval[] = $this->parseRCType($t);
+ foreach ( $type as $t )
+ $retval[] = $this->parseRCType( $t );
return $retval;
}
- switch($type)
+ switch( $type )
{
case 'edit': return RC_EDIT;
case 'new': return RC_NEW;
@@ -364,6 +433,10 @@ class ApiQueryRecentChanges extends ApiQueryBase {
if ( isset( $params['token'] ) ) {
return 'private';
}
+ if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
+ // formatComment() calls wfMsg() among other things
+ return 'anon-public-user-private';
+ }
return 'public';
}
@@ -386,12 +459,20 @@ class ApiQueryRecentChanges extends ApiQueryBase {
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => 'namespace'
),
+ 'user' => array(
+ ApiBase :: PARAM_TYPE => 'user'
+ ),
+ 'excludeuser' => array(
+ ApiBase :: PARAM_TYPE => 'user'
+ ),
+ 'tag' => null,
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_DFLT => 'title|timestamp|ids',
ApiBase :: PARAM_TYPE => array (
'user',
'comment',
+ 'parsedcomment',
'flags',
'timestamp',
'title',
@@ -400,10 +481,11 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'redirect',
'patrolled',
'loginfo',
+ 'tags'
)
),
'token' => array(
- ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()),
+ ApiBase :: PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
ApiBase :: PARAM_ISMULTI => true
),
'show' => array (
@@ -445,6 +527,8 @@ 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)',
+ 'user' => 'Only list changes by this user',
+ 'excludeuser' => 'Don\'t list changes by this user',
'prop' => 'Include additional pieces of information',
'token' => 'Which tokens to obtain for each change',
'show' => array (
@@ -452,13 +536,22 @@ class ApiQueryRecentChanges extends ApiQueryBase {
'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 changes to return.'
+ 'limit' => 'How many total changes to return.',
+ 'tag' => 'Only list changes tagged with this tag.',
);
}
public function getDescription() {
return 'Enumerate recent changes';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'show' ),
+ array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ),
+ array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -467,6 +560,6 @@ class ApiQueryRecentChanges extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index eba526a3..6166b6a2 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -37,12 +37,12 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryRevisions extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'rv');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'rv' );
}
private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
- $fld_comment = false, $fld_user = false, $fld_content = false;
+ $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_content = false, $fld_tags = false;
protected function getTokenFunctions() {
// tokenname => function
@@ -50,40 +50,40 @@ class ApiQueryRevisions extends ApiQueryBase {
// should return token or false
// Don't call the hooks twice
- if(isset($this->tokenFunctions))
+ 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')))
+ if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) )
return array();
$this->tokenFunctions = array(
'rollback' => array( 'ApiQueryRevisions', 'getRollbackToken' )
);
- wfRunHooks('APIQueryRevisionsTokens', array(&$this->tokenFunctions));
+ wfRunHooks( 'APIQueryRevisionsTokens', array( &$this->tokenFunctions ) );
return $this->tokenFunctions;
}
- public static function getRollbackToken($pageid, $title, $rev)
+ public static function getRollbackToken( $pageid, $title, $rev )
{
global $wgUser;
- if(!$wgUser->isAllowed('rollback'))
+ if ( !$wgUser->isAllowed( 'rollback' ) )
return false;
- return $wgUser->editToken(array($title->getPrefixedText(),
- $rev->getUserText()));
+ return $wgUser->editToken( array( $title->getPrefixedText(),
+ $rev->getUserText() ) );
}
public function execute() {
- $params = $this->extractRequestParams(false);
+ $params = $this->extractRequestParams( false );
// If any of those parameters are used, work in 'enumeration' mode.
// Enum mode can only be used when exactly one page is provided.
// Enumerating revisions on multiple pages make it extremely
// difficult to manage continuations and require additional SQL indexes
- $enumRevMode = (!is_null($params['user']) || !is_null($params['excludeuser']) ||
- !is_null($params['limit']) || !is_null($params['startid']) ||
- !is_null($params['endid']) || $params['dir'] === 'newer' ||
- !is_null($params['start']) || !is_null($params['end']));
+ $enumRevMode = ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ||
+ !is_null( $params['limit'] ) || !is_null( $params['startid'] ) ||
+ !is_null( $params['endid'] ) || $params['dir'] === 'newer' ||
+ !is_null( $params['start'] ) || !is_null( $params['end'] ) );
$pageSet = $this->getPageSet();
@@ -91,77 +91,99 @@ class ApiQueryRevisions extends ApiQueryBase {
$revCount = $pageSet->getRevisionCount();
// Optimization -- nothing to do
- if ($revCount === 0 && $pageCount === 0)
+ if ( $revCount === 0 && $pageCount === 0 )
return;
- if ($revCount > 0 && $enumRevMode)
- $this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids');
+ if ( $revCount > 0 && $enumRevMode )
+ $this->dieUsage( 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids' );
- if ($pageCount > 1 && $enumRevMode)
- $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.', 'multpages');
+ if ( $pageCount > 1 && $enumRevMode )
+ $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' );
- if (!is_null($params['diffto'])) {
- if ($params['diffto'] == 'cur')
+ $this->diffto = $this->difftotext = null;
+ if ( !is_null( $params['difftotext'] ) ) {
+ $this->difftotext = $params['difftotext'];
+ } else if ( !is_null( $params['diffto'] ) ) {
+ if ( $params['diffto'] == 'cur' )
$params['diffto'] = 0;
- if ((!ctype_digit($params['diffto']) || $params['diffto'] < 0)
- && $params['diffto'] != 'prev' && $params['diffto'] != 'next')
- $this->dieUsage('rvdiffto must be set to a non-negative number, "prev", "next" or "cur"', 'diffto');
+ if ( ( !ctype_digit( $params['diffto'] ) || $params['diffto'] < 0 )
+ && $params['diffto'] != 'prev' && $params['diffto'] != 'next' )
+ $this->dieUsage( 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"', 'diffto' );
// Check whether the revision exists and is readable,
// DifferenceEngine returns a rather ambiguous empty
// string if that's not the case
- if ($params['diffto'] != 0) {
- $difftoRev = Revision::newFromID($params['diffto']);
- if (!$difftoRev)
- $this->dieUsageMsg(array('nosuchrevid', $params['diffto']));
- if (!$difftoRev->userCan(Revision::DELETED_TEXT)) {
- $this->setWarning("Couldn't diff to r{$difftoRev->getID()}: content is hidden");
+ if ( $params['diffto'] != 0 ) {
+ $difftoRev = Revision::newFromID( $params['diffto'] );
+ if ( !$difftoRev )
+ $this->dieUsageMsg( array( 'nosuchrevid', $params['diffto'] ) );
+ if ( !$difftoRev->userCan( Revision::DELETED_TEXT ) ) {
+ $this->setWarning( "Couldn't diff to r{$difftoRev->getID()}: content is hidden" );
$params['diffto'] = null;
}
}
+ $this->diffto = $params['diffto'];
}
- $this->addTables('revision');
- $this->addFields(Revision::selectFields());
- $this->addTables('page');
- $this->addWhere('page_id = rev_page');
+ $db = $this->getDB();
+ $this->addTables( array( 'page', 'revision' ) );
+ $this->addFields( Revision::selectFields() );
+ $this->addWhere( 'page_id = rev_page' );
- $prop = array_flip($params['prop']);
+ $prop = array_flip( $params['prop'] );
// Optional fields
- $this->fld_ids = isset ($prop['ids']);
+ $this->fld_ids = isset ( $prop['ids'] );
// $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed?
- $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->fld_flags = isset ( $prop['flags'] );
+ $this->fld_timestamp = isset ( $prop['timestamp'] );
+ $this->fld_comment = isset ( $prop['comment'] );
+ $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_size = isset ( $prop['size'] );
+ $this->fld_user = isset ( $prop['user'] );
$this->token = $params['token'];
- $this->diffto = $params['diffto'];
- if ( !is_null($this->token) || $pageCount > 0) {
+ // Possible indexes used
+ $index = array();
+
+ if ( !is_null( $this->token ) || $pageCount > 0 ) {
$this->addFields( Revision::selectPageFields() );
}
- if (isset ($prop['content'])) {
+ if ( isset ( $prop['tags'] ) ) {
+ $this->fld_tags = true;
+ $this->addTables( 'tag_summary' );
+ $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) );
+ $this->addFields( 'ts_tags' );
+ }
+
+ if ( !is_null( $params['tag'] ) ) {
+ $this->addTables( 'change_tag' );
+ $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) );
+ $this->addWhereFld( 'ct_tag' , $params['tag'] );
+ global $wgOldChangeTagsIndex;
+ $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
+ }
+
+ if ( isset( $prop['content'] ) || !is_null( $this->difftotext ) ) {
// For each page we will request, the user must have read rights for that page
- foreach ($pageSet->getGoodTitles() as $title) {
- if( !$title->userCanRead() )
+ foreach ( $pageSet->getGoodTitles() as $title ) {
+ if ( !$title->userCanRead() )
$this->dieUsage(
'The current user is not allowed to read ' . $title->getPrefixedText(),
- 'accessdenied');
+ 'accessdenied' );
}
- $this->addTables('text');
- $this->addWhere('rev_text_id=old_id');
- $this->addFields('old_id');
- $this->addFields(Revision::selectTextFields());
+ $this->addTables( 'text' );
+ $this->addWhere( 'rev_text_id=old_id' );
+ $this->addFields( 'old_id' );
+ $this->addFields( Revision::selectTextFields() );
- $this->fld_content = true;
+ $this->fld_content = isset( $prop['content'] );
$this->expandTemplates = $params['expandtemplates'];
$this->generateXML = $params['generatexml'];
- if(isset($params['section']))
+ if ( isset( $params['section'] ) )
$this->section = $params['section'];
else
$this->section = false;
@@ -170,22 +192,22 @@ class ApiQueryRevisions extends ApiQueryBase {
$userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 );
$botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 );
$limit = $params['limit'];
- if( $limit == 'max' ) {
+ if ( $limit == 'max' ) {
$limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$this->getResult()->addValue( 'limits', $this->getModuleName(), $limit );
}
- if ($enumRevMode) {
+ if ( $enumRevMode ) {
// This is mostly to prevent parameter errors (and optimize SQL?)
- if (!is_null($params['startid']) && !is_null($params['start']))
- $this->dieUsage('start and startid cannot be used together', 'badparams');
+ if ( !is_null( $params['startid'] ) && !is_null( $params['start'] ) )
+ $this->dieUsage( 'start and startid cannot be used together', 'badparams' );
- if (!is_null($params['endid']) && !is_null($params['end']))
- $this->dieUsage('end and endid cannot be used together', 'badparams');
+ if ( !is_null( $params['endid'] ) && !is_null( $params['end'] ) )
+ $this->dieUsage( 'end and endid cannot be used together', 'badparams' );
- if(!is_null($params['user']) && !is_null($params['excludeuser']))
- $this->dieUsage('user and excludeuser cannot be used together', 'badparams');
+ if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) )
+ $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' );
// This code makes an assumption that sorting by rev_id and rev_timestamp produces
// the same result. This way users may request revisions starting at a given time,
@@ -194,187 +216,212 @@ class ApiQueryRevisions extends ApiQueryBase {
// one row with the same timestamp for the same page.
// The order needs to be the same as start parameter to avoid SQL filesort.
- if (is_null($params['startid']) && is_null($params['endid']))
- $this->addWhereRange('rev_timestamp', $params['dir'],
- $params['start'], $params['end']);
+ if ( is_null( $params['startid'] ) && is_null( $params['endid'] ) )
+ $this->addWhereRange( 'rev_timestamp', $params['dir'],
+ $params['start'], $params['end'] );
else {
- $this->addWhereRange('rev_id', $params['dir'],
- $params['startid'], $params['endid']);
+ $this->addWhereRange( 'rev_id', $params['dir'],
+ $params['startid'], $params['endid'] );
// One of start and end can be set
// If neither is set, this does nothing
- $this->addWhereRange('rev_timestamp', $params['dir'],
- $params['start'], $params['end'], false);
+ $this->addWhereRange( 'rev_timestamp', $params['dir'],
+ $params['start'], $params['end'], false );
}
// must manually initialize unset limit
- if (is_null($limit))
+ if ( is_null( $limit ) )
$limit = 10;
- $this->validateLimit('limit', $limit, 1, $userMax, $botMax);
+ $this->validateLimit( 'limit', $limit, 1, $userMax, $botMax );
// There is only one ID, use it
- $this->addWhereFld('rev_page', reset(array_keys($pageSet->getGoodTitles())));
-
- if(!is_null($params['user'])) {
- $this->addWhereFld('rev_user_text', $params['user']);
- } elseif (!is_null($params['excludeuser'])) {
- $this->addWhere('rev_user_text != ' .
- $this->getDB()->addQuotes($params['excludeuser']));
+ $ids = array_keys( $pageSet->getGoodTitles() );
+ $this->addWhereFld( 'rev_page', reset( $ids ) );
+
+ if ( !is_null( $params['user'] ) ) {
+ $this->addWhereFld( 'rev_user_text', $params['user'] );
+ } elseif ( !is_null( $params['excludeuser'] ) ) {
+ $this->addWhere( 'rev_user_text != ' .
+ $db->addQuotes( $params['excludeuser'] ) );
}
- if(!is_null($params['user']) || !is_null($params['excludeuser'])) {
+ if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
// Paranoia: avoid brute force searches (bug 17342)
- $this->addWhere('rev_deleted & ' . Revision::DELETED_USER . ' = 0');
+ $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
}
}
- elseif ($revCount > 0) {
+ elseif ( $revCount > 0 ) {
$max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$revs = $pageSet->getRevisionIDs();
- if(self::truncateArray($revs, $max))
- $this->setWarning("Too many values supplied for parameter 'revids': the limit is $max");
+ if ( self::truncateArray( $revs, $max ) )
+ $this->setWarning( "Too many values supplied for parameter 'revids': the limit is $max" );
// Get all revision IDs
- $this->addWhereFld('rev_id', array_keys($revs));
+ $this->addWhereFld( 'rev_id', array_keys( $revs ) );
- if(!is_null($params['continue']))
- $this->addWhere("rev_id >= '" . intval($params['continue']) . "'");
- $this->addOption('ORDER BY', 'rev_id');
+ if ( !is_null( $params['continue'] ) )
+ $this->addWhere( "rev_id >= '" . intval( $params['continue'] ) . "'" );
+ $this->addOption( 'ORDER BY', 'rev_id' );
// assumption testing -- we should never get more then $revCount rows.
$limit = $revCount;
}
- elseif ($pageCount > 0) {
+ elseif ( $pageCount > 0 ) {
$max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
$titles = $pageSet->getGoodTitles();
- if(self::truncateArray($titles, $max))
- $this->setWarning("Too many values supplied for parameter 'titles': the limit is $max");
+ if ( self::truncateArray( $titles, $max ) )
+ $this->setWarning( "Too many values supplied for parameter 'titles': the limit is $max" );
// When working in multi-page non-enumeration mode,
// limit to the latest revision only
- $this->addWhere('page_id=rev_page');
- $this->addWhere('page_latest=rev_id');
+ $this->addWhere( 'page_id=rev_page' );
+ $this->addWhere( 'page_latest=rev_id' );
// Get all page IDs
- $this->addWhereFld('page_id', array_keys($titles));
+ $this->addWhereFld( 'page_id', array_keys( $titles ) );
// Every time someone relies on equality propagation, god kills a kitten :)
- $this->addWhereFld('rev_page', array_keys($titles));
+ $this->addWhereFld( 'rev_page', array_keys( $titles ) );
- if(!is_null($params['continue']))
+ 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");
- $pageid = intval($cont[0]);
- $revid = intval($cont[1]);
- $this->addWhere("rev_page > '$pageid' OR " .
+ $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" );
+ $pageid = intval( $cont[0] );
+ $revid = intval( $cont[1] );
+ $this->addWhere( "rev_page > '$pageid' OR " .
"(rev_page = '$pageid' AND " .
- "rev_id >= '$revid')");
+ "rev_id >= '$revid')" );
}
- $this->addOption('ORDER BY', 'rev_page, rev_id');
+ $this->addOption( 'ORDER BY', 'rev_page, rev_id' );
// assumption testing -- we should never get more then $pageCount rows.
$limit = $pageCount;
} else
- ApiBase :: dieDebug(__METHOD__, 'param validation?');
+ ApiBase :: dieDebug( __METHOD__, 'param validation?' );
- $this->addOption('LIMIT', $limit +1);
+ $this->addOption( 'LIMIT', $limit + 1 );
+ $this->addOption( 'USE INDEX', $index );
$data = array ();
$count = 0;
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
- $db = $this->getDB();
- while ($row = $db->fetchObject($res)) {
+ while ( $row = $db->fetchObject( $res ) ) {
- if (++ $count > $limit) {
+ if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- if (!$enumRevMode)
- ApiBase :: dieDebug(__METHOD__, 'Got more rows then expected'); // bug report
- $this->setContinueEnumParameter('startid', intval($row->rev_id));
+ if ( !$enumRevMode )
+ ApiBase :: dieDebug( __METHOD__, 'Got more rows then expected' ); // bug report
+ $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) );
break;
}
- $revision = new Revision( $row );
+
//
- $fit = $this->addPageSubItem($revision->getPage(), $this->extractRowInfo($revision), 'rev');
- if(!$fit)
+ $fit = $this->addPageSubItem( $row->rev_page, $this->extractRowInfo( $row ), 'rev' );
+ if ( !$fit )
{
- if($enumRevMode)
- $this->setContinueEnumParameter('startid', intval($row->rev_id));
- else if($revCount > 0)
- $this->setContinueEnumParameter('continue', intval($row->rev_id));
+ if ( $enumRevMode )
+ $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) );
+ else if ( $revCount > 0 )
+ $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) );
else
- $this->setContinueEnumParameter('continue', intval($row->rev_page) .
- '|' . intval($row->rev_id));
+ $this->setContinueEnumParameter( 'continue', intval( $row->rev_page ) .
+ '|' . intval( $row->rev_id ) );
break;
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
}
- private function extractRowInfo( $revision ) {
+ private function extractRowInfo( $row ) {
+ $revision = new Revision( $row );
$title = $revision->getTitle();
$vals = array ();
- if ($this->fld_ids) {
- $vals['revid'] = intval($revision->getId());
+ if ( $this->fld_ids ) {
+ $vals['revid'] = intval( $revision->getId() );
// $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed?
+ if ( !is_null( $revision->getParentId() ) )
+ $vals['parentid'] = intval( $revision->getParentId() );
}
- if ($this->fld_flags && $revision->isMinor())
+ if ( $this->fld_flags && $revision->isMinor() )
$vals['minor'] = '';
- if ($this->fld_user) {
- if ($revision->isDeleted(Revision::DELETED_USER)) {
+ if ( $this->fld_user ) {
+ if ( $revision->isDeleted( Revision::DELETED_USER ) ) {
$vals['userhidden'] = '';
} else {
$vals['user'] = $revision->getUserText();
- if (!$revision->getUser())
+ if ( !$revision->getUser() )
$vals['anon'] = '';
}
}
- if ($this->fld_timestamp) {
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $revision->getTimestamp());
+ if ( $this->fld_timestamp ) {
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $revision->getTimestamp() );
}
- if ($this->fld_size && !is_null($revision->getSize())) {
- $vals['size'] = intval($revision->getSize());
+ if ( $this->fld_size && !is_null( $revision->getSize() ) ) {
+ $vals['size'] = intval( $revision->getSize() );
}
- if ($this->fld_comment) {
- if ($revision->isDeleted(Revision::DELETED_COMMENT)) {
+ if ( $this->fld_comment || $this->fld_parsedcomment ) {
+ if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
$vals['commenthidden'] = '';
} else {
$comment = $revision->getComment();
- if (strval($comment) !== '')
- $vals['comment'] = $comment;
+ if ( strval( $comment ) !== '' )
+ {
+ if ( $this->fld_comment )
+ $vals['comment'] = $comment;
+
+ if ( $this->fld_parsedcomment ) {
+ global $wgUser;
+ $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $comment, $title );
+ }
+ }
}
- }
+ }
- if(!is_null($this->token))
+ if ( $this->fld_tags ) {
+ if ( $row->ts_tags ) {
+ $tags = explode( ',', $row->ts_tags );
+ $this->getResult()->setIndexedTagName( $tags, 'tag' );
+ $vals['tags'] = $tags;
+ } else {
+ $vals['tags'] = array();
+ }
+ }
+
+ if ( !is_null( $this->token ) )
{
$tokenFunctions = $this->getTokenFunctions();
- foreach($this->token as $t)
+ 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");
+ $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 && !$revision->isDeleted(Revision::DELETED_TEXT)) {
+ $text = null;
+ if ( $this->fld_content || !is_null( $this->difftotext ) ) {
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');
+ // 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->generateXML) {
+ }
+ if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
+ if ( $this->generateXML ) {
$wgParser->startExternalParse( $title, new ParserOptions(), OT_PREPROCESS );
$dom = $wgParser->preprocessToDom( $text );
if ( is_callable( array( $dom, 'saveXML' ) ) ) {
@@ -385,24 +432,30 @@ class ApiQueryRevisions extends ApiQueryBase {
$vals['parsetree'] = $xml;
}
- if ($this->expandTemplates) {
+ if ( $this->expandTemplates ) {
$text = $wgParser->preprocess( $text, $title, new ParserOptions() );
}
- ApiResult :: setContent($vals, $text);
- } else if ($this->fld_content) {
+ ApiResult :: setContent( $vals, $text );
+ } else if ( $this->fld_content ) {
$vals['texthidden'] = '';
}
- if (!is_null($this->diffto)) {
+ if ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
global $wgAPIMaxUncachedDiffs;
- static $n = 0; // Numer of uncached diffs we've had
- if($n< $wgAPIMaxUncachedDiffs) {
- $engine = new DifferenceEngine($title, $revision->getID(), $this->diffto);
+ static $n = 0; // Number of uncached diffs we've had
+ if ( $n < $wgAPIMaxUncachedDiffs ) {
+ $vals['diff'] = array();
+ if ( !is_null( $this->difftotext ) ) {
+ $engine = new DifferenceEngine( $title );
+ $engine->setText( $text, $this->difftotext );
+ } else {
+ $engine = new DifferenceEngine( $title, $revision->getID(), $this->diffto );
+ $vals['diff']['from'] = $engine->getOldid();
+ $vals['diff']['to'] = $engine->getNewid();
+ }
$difftext = $engine->getDiffBody();
- $vals['diff']['from'] = $engine->getOldid();
- $vals['diff']['to'] = $engine->getNewid();
- ApiResult::setContent($vals['diff'], $difftext);
- if(!$engine->wasCacheHit())
+ ApiResult::setContent( $vals['diff'], $difftext );
+ if ( !$engine->wasCacheHit() )
$n++;
} else {
$vals['diff']['notcached'] = '';
@@ -434,7 +487,9 @@ class ApiQueryRevisions extends ApiQueryBase {
'user',
'size',
'comment',
+ 'parsedcomment',
'content',
+ 'tags'
)
),
'limit' => array (
@@ -468,36 +523,41 @@ class ApiQueryRevisions extends ApiQueryBase {
'excludeuser' => array(
ApiBase :: PARAM_TYPE => 'user'
),
+ 'tag' => null,
'expandtemplates' => false,
'generatexml' => false,
'section' => null,
'token' => array(
- ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()),
+ ApiBase :: PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
ApiBase :: PARAM_ISMULTI => true
),
'continue' => null,
'diffto' => null,
+ 'difftotext' => null,
);
}
public function getParamDescription() {
return array (
'prop' => 'Which properties to get for each revision.',
- 'limit' => 'limit how many revisions will be returned (enum)',
- 'startid' => 'from which revision id to start enumeration (enum)',
- 'endid' => 'stop revision enumeration on this revid (enum)',
- 'start' => 'from which revision timestamp to start enumeration (enum)',
- 'end' => 'enumerate up to this timestamp (enum)',
- 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)',
- 'user' => 'only include revisions made by user',
- 'excludeuser' => 'exclude revisions made by user',
- 'expandtemplates' => 'expand templates in revision content',
- 'generatexml' => 'generate XML parse tree for revision content',
- 'section' => 'only retrieve the content of this section',
+ 'limit' => 'Limit how many revisions will be returned (enum)',
+ 'startid' => 'From which revision id to start enumeration (enum)',
+ 'endid' => 'Stop revision enumeration on this revid (enum)',
+ 'start' => 'From which revision timestamp to start enumeration (enum)',
+ 'end' => 'Enumerate up to this timestamp (enum)',
+ 'dir' => 'Direction of enumeration - towards "newer" or "older" revisions (enum)',
+ 'user' => 'Only include revisions made by user',
+ 'excludeuser' => 'Exclude revisions made by user',
+ 'expandtemplates' => 'Expand templates in revision content',
+ 'generatexml' => 'Generate XML parse tree for revision content',
+ 'section' => 'Only retrieve the content of this section',
'token' => 'Which tokens to obtain for each revision',
'continue' => 'When more results are available, use this to continue',
- 'diffto' => array('Revision ID to diff each revision to.',
- 'Use "prev", "next" and "cur" for the previous, next and current revision respectively.'),
+ 'diffto' => array( 'Revision ID to diff each revision to.',
+ 'Use "prev", "next" and "cur" for the previous, next and current revision respectively.' ),
+ 'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.',
+ 'Overrides diffto. If rvsection is set, only that section will be diffed against this text.' ),
+ 'tag' => 'Only list revisions tagged with this tag',
);
}
@@ -511,6 +571,19 @@ class ApiQueryRevisions extends ApiQueryBase {
'All parameters marked as (enum) may only be used with a single page (#2).'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'nosuchrevid', 'diffto' ),
+ array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).' ),
+ array( 'code' => 'multpages', 'info' => '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.' ),
+ array( 'code' => 'diffto', 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' ),
+ array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ),
+ array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ),
+ array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ),
+ array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -530,6 +603,6 @@ class ApiQueryRevisions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryRevisions.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryRevisions.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index a8f3fcc8..4e032321 100644
--- a/includes/api/ApiQuerySearch.php
+++ b/includes/api/ApiQuerySearch.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,36 +35,42 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQuerySearch extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'sr');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'sr' );
}
public function execute() {
$this->run();
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
-
+ private function run( $resultPageSet = null ) {
+ global $wgContLang;
$params = $this->extractRequestParams();
+ // Extract parameters
$limit = $params['limit'];
$query = $params['search'];
$what = $params['what'];
- if (strval($query) === '')
- $this->dieUsage("empty search string is not allowed", 'param-search');
+ $searchInfo = array_flip( $params['info'] );
+ $prop = array_flip( $params['prop'] );
+
+ if ( strval( $query ) === '' )
+ $this->dieUsage( "empty search string is not allowed", 'param-search' );
+ // Create search engine instance and set options
$search = SearchEngine::create();
- $search->setLimitOffset( $limit+1, $params['offset'] );
+ $search->setLimitOffset( $limit + 1, $params['offset'] );
$search->setNamespaces( $params['namespace'] );
$search->showRedirects = $params['redirects'];
- if ($what == 'text') {
+ // Perform the actual search
+ if ( $what == 'text' ) {
$matches = $search->searchText( $query );
- } elseif( $what == 'title' ) {
+ } elseif ( $what == 'title' ) {
$matches = $search->searchTitle( $query );
} else {
// We default to title searches; this is a terrible legacy
@@ -78,36 +84,61 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
// for instance the Lucene-based engine we use on Wikipedia.
// In this case, fall back to full-text search (which will
// include titles in it!)
- if( is_null( $matches ) ) {
+ if ( is_null( $matches ) ) {
$what = 'text';
$matches = $search->searchText( $query );
}
}
- if (is_null($matches))
- $this->dieUsage("{$what} search is disabled",
- "search-{$what}-disabled");
+ if ( is_null( $matches ) )
+ $this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" );
+
+ // Add search meta data to result
+ if ( isset( $searchInfo['totalhits'] ) ) {
+ $totalhits = $matches->getTotalHits();
+ if ( $totalhits !== null ) {
+ $this->getResult()->addValue( array( 'query', 'searchinfo' ),
+ 'totalhits', $totalhits );
+ }
+ }
+ if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) {
+ $this->getResult()->addValue( array( 'query', 'searchinfo' ),
+ 'suggestion', $matches->getSuggestionQuery() );
+ }
+ // Add the search results to the result
+ $terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
$titles = array ();
$count = 0;
- while( $result = $matches->next() ) {
- if (++ $count > $limit) {
+ while ( $result = $matches->next() ) {
+ if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional items to be had. Stop here...
- $this->setContinueEnumParameter('offset', $params['offset'] + $params['limit']);
+ $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] );
break;
}
// Silently skip broken and missing titles
- if ($result->isBrokenTitle() || $result->isMissingRevision())
+ if ( $result->isBrokenTitle() || $result->isMissingRevision() )
continue;
$title = $result->getTitle();
- if (is_null($resultPageSet)) {
+ if ( is_null( $resultPageSet ) ) {
$vals = array();
- ApiQueryBase::addTitleInfo($vals, $title);
- $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
- {
- $this->setContinueEnumParameter('offset', $params['offset'] + $count - 1);
+ ApiQueryBase::addTitleInfo( $vals, $title );
+
+ if ( isset( $prop['snippet'] ) )
+ $vals['snippet'] = $result->getTextSnippet( $terms );
+ if ( isset( $prop['size'] ) )
+ $vals['size'] = $result->getByteSize();
+ if ( isset( $prop['wordcount'] ) )
+ $vals['wordcount'] = $result->getWordCount();
+ if ( isset( $prop['timestamp'] ) )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() );
+
+ // Add item to results and see whether it fits
+ $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ),
+ null, $vals );
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'offset', $params['offset'] + $count - 1 );
break;
}
} else {
@@ -115,10 +146,12 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
}
- if (is_null($resultPageSet)) {
- $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'p');
+ if ( is_null( $resultPageSet ) ) {
+ $this->getResult()->setIndexedTagName_internal( array(
+ 'query', $this->getModuleName()
+ ), 'p' );
} else {
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
}
@@ -141,14 +174,32 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
'text',
)
),
+ 'info' => array(
+ ApiBase :: PARAM_DFLT => 'totalhits|suggestion',
+ ApiBase :: PARAM_TYPE => array (
+ 'totalhits',
+ 'suggestion',
+ ),
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
+ 'prop' => array(
+ ApiBase :: PARAM_DFLT => 'size|wordcount|timestamp|snippet',
+ ApiBase :: PARAM_TYPE => array (
+ 'size',
+ 'wordcount',
+ 'timestamp',
+ 'snippet',
+ ),
+ ApiBase :: PARAM_ISMULTI => true,
+ ),
'redirects' => false,
'offset' => 0,
'limit' => array (
ApiBase :: PARAM_DFLT => 10,
ApiBase :: PARAM_TYPE => 'limit',
ApiBase :: PARAM_MIN => 1,
- ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1,
- ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2
+ ApiBase :: PARAM_MAX => ApiBase :: LIMIT_SML1,
+ ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_SML2
)
);
}
@@ -158,6 +209,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
'search' => 'Search for all page titles (or content) that has this value.',
'namespace' => 'The namespace(s) to enumerate.',
'what' => 'Search inside the text or titles.',
+ 'info' => 'What metadata to return.',
+ 'prop' => 'What properties to return.',
'redirects' => 'Include redirect pages in the search.',
'offset' => 'Use this value to continue paging (return by query)',
'limit' => 'How many total pages to return.'
@@ -167,6 +220,14 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
public function getDescription() {
return 'Perform a full text search';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'param-search', 'info' => 'empty search string is not allowed' ),
+ array( 'code' => 'search-text-disabled', 'info' => 'text search is disabled' ),
+ array( 'code' => 'search-title-disabled', 'info' => 'title search is disabled' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -177,6 +238,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySearch.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQuerySearch.php 69932 2010-07-26 08:03:21Z tstarling $';
}
-} \ No newline at end of file
+}
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index 623855f6..0385e192 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -23,7 +23,7 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if( !defined('MEDIAWIKI') ) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
require_once( 'ApiQueryBase.php' );
}
@@ -42,7 +42,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
public function execute() {
$params = $this->extractRequestParams();
$done = array();
- foreach( $params['prop'] as $p )
+ foreach ( $params['prop'] as $p )
{
switch ( $p )
{
@@ -72,7 +72,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$fit = $this->appendStatistics( $p );
break;
case 'usergroups':
- $fit = $this->appendUserGroups( $p );
+ $fit = $this->appendUserGroups( $p, $params['numberingroup'] );
break;
case 'extensions':
$fit = $this->appendExtensions( $p );
@@ -83,15 +83,18 @@ class ApiQuerySiteinfo extends ApiQueryBase {
case 'rightsinfo':
$fit = $this->appendRightsInfo( $p );
break;
+ case 'languages':
+ $fit = $this->appendLanguages( $p );
+ break;
default :
ApiBase :: dieDebug( __METHOD__, "Unknown prop=$p" );
}
- if(!$fit)
+ if ( !$fit )
{
- # Abuse siprop as a query-continue parameter
- # and set it to all unprocessed props
- $this->setContinueEnumParameter('prop', implode('|',
- array_diff($params['prop'], $done)));
+ // Abuse siprop as a query-continue parameter
+ // and set it to all unprocessed props
+ $this->setContinueEnumParameter( 'prop', implode( '|',
+ array_diff( $params['prop'], $done ) ) );
break;
}
$done[] = $p;
@@ -99,45 +102,59 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
protected function appendGeneralInfo( $property ) {
- global $wgSitename, $wgVersion, $wgCapitalLinks, $wgRightsCode, $wgRightsText, $wgContLang;
- global $wgLanguageCode, $IP, $wgEnableWriteAPI, $wgLang, $wgLocaltimezone, $wgLocalTZoffset;
+ global $wgContLang;
+ global $wgLang;
$data = array();
- $mainPage = Title :: newFromText(wfMsgForContent('mainpage'));
+ $mainPage = Title :: newFromText( wfMsgForContent( 'mainpage' ) );
$data['mainpage'] = $mainPage->getPrefixedText();
$data['base'] = $mainPage->getFullUrl();
- $data['sitename'] = $wgSitename;
- $data['generator'] = "MediaWiki $wgVersion";
-
- $svn = SpecialVersion::getSvnRevision( $IP );
- if( $svn )
+ $data['sitename'] = $GLOBALS['wgSitename'];
+ $data['generator'] = "MediaWiki {$GLOBALS['wgVersion']}";
+ $data['phpversion'] = phpversion();
+ $data['phpsapi'] = php_sapi_name();
+ $data['dbtype'] = $GLOBALS['wgDBtype'];
+ $data['dbversion'] = $this->getDB()->getServerVersion();
+
+ $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] );
+ if ( $svn )
$data['rev'] = $svn;
- $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future
+ // 'case-insensitive' option is reserved for future
+ $data['case'] = $GLOBALS['wgCapitalLinks'] ? 'first-letter' : 'case-sensitive';
- if( isset( $wgRightsCode ) )
- $data['rightscode'] = $wgRightsCode;
- $data['rights'] = $wgRightsText;
- $data['lang'] = $wgLanguageCode;
- if( $wgContLang->isRTL() )
+ if ( isset( $GLOBALS['wgRightsCode'] ) )
+ $data['rightscode'] = $GLOBALS['wgRightsCode'];
+ $data['rights'] = $GLOBALS['wgRightsText'];
+ $data['lang'] = $GLOBALS['wgLanguageCode'];
+ if ( $wgContLang->isRTL() )
$data['rtl'] = '';
$data['fallback8bitEncoding'] = $wgLang->fallback8bitEncoding();
- if( wfReadOnly() )
+ if ( wfReadOnly() ) {
$data['readonly'] = '';
- if( $wgEnableWriteAPI )
+ $data['readonlyreason'] = wfReadOnlyReason();
+ }
+ if ( $GLOBALS['wgEnableWriteAPI'] )
$data['writeapi'] = '';
- $tz = $wgLocaltimezone;
- $offset = $wgLocalTZoffset;
- if( is_null( $tz ) ) {
+ $tz = $GLOBALS['wgLocaltimezone'];
+ $offset = $GLOBALS['wgLocalTZoffset'];
+ if ( is_null( $tz ) ) {
$tz = 'UTC';
$offset = 0;
- } elseif( is_null( $offset ) ) {
+ } elseif ( is_null( $offset ) ) {
$offset = 0;
}
$data['timezone'] = $tz;
- $data['timeoffset'] = intval($offset);
+ $data['timeoffset'] = intval( $offset );
+ $data['articlepath'] = $GLOBALS['wgArticlePath'];
+ $data['scriptpath'] = $GLOBALS['wgScriptPath'];
+ $data['script'] = $GLOBALS['wgScript'];
+ $data['variantarticlepath'] = $GLOBALS['wgVariantArticlePath'];
+ $data['server'] = $GLOBALS['wgServer'];
+ $data['wikiid'] = wfWikiID();
+ $data['time'] = wfTimestamp( TS_ISO_8601, time() );
return $this->getResult()->addValue( 'query', $property, $data );
}
@@ -145,19 +162,23 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendNamespaces( $property ) {
global $wgContLang;
$data = array();
- foreach( $wgContLang->getFormattedNamespaces() as $ns => $title )
+ foreach ( $wgContLang->getFormattedNamespaces() as $ns => $title )
{
$data[$ns] = array(
- 'id' => intval($ns)
+ 'id' => intval( $ns ),
+ 'case' => MWNamespace::isCapitalized( $ns ) ? 'first-letter' : 'case-sensitive',
);
ApiResult :: setContent( $data[$ns], $title );
$canonical = MWNamespace::getCanonicalName( $ns );
- if( MWNamespace::hasSubpages( $ns ) )
+ if ( MWNamespace::hasSubpages( $ns ) )
$data[$ns]['subpages'] = '';
- if( $canonical )
- $data[$ns]['canonical'] = strtr($canonical, '_', ' ');
+ if ( $canonical )
+ $data[$ns]['canonical'] = strtr( $canonical, '_', ' ' );
+
+ if ( MWNamespace::isContent( $ns ) )
+ $data[$ns]['content'] = '';
}
$this->getResult()->setIndexedTagName( $data, 'ns' );
@@ -166,17 +187,16 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendNamespaceAliases( $property ) {
global $wgNamespaceAliases, $wgContLang;
- $wgContLang->load();
- $aliases = array_merge( $wgNamespaceAliases, $wgContLang->namespaceAliases );
+ $aliases = array_merge( $wgNamespaceAliases, $wgContLang->getNamespaceAliases() );
$namespaces = $wgContLang->getNamespaces();
$data = array();
- foreach( $aliases as $title => $ns ) {
- if( $namespaces[$ns] == $title ) {
+ foreach ( $aliases as $title => $ns ) {
+ if ( $namespaces[$ns] == $title ) {
// Don't list duplicates
continue;
}
$item = array(
- 'id' => intval($ns)
+ 'id' => intval( $ns )
);
ApiResult :: setContent( $item, strtr( $title, '_', ' ' ) );
$data[] = $item;
@@ -189,7 +209,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendSpecialPageAliases( $property ) {
global $wgLang;
$data = array();
- foreach( $wgLang->getSpecialPageAliases() as $specialpage => $aliases )
+ foreach ( $wgLang->getSpecialPageAliases() as $specialpage => $aliases )
{
$arr = array( 'realname' => $specialpage, 'aliases' => $aliases );
$this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
@@ -202,16 +222,16 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendMagicWords( $property ) {
global $wgContLang;
$data = array();
- foreach($wgContLang->getMagicWords() as $magicword => $aliases)
+ foreach ( $wgContLang->getMagicWords() as $magicword => $aliases )
{
- $caseSensitive = array_shift($aliases);
- $arr = array('name' => $magicword, 'aliases' => $aliases);
- if($caseSensitive)
+ $caseSensitive = array_shift( $aliases );
+ $arr = array( 'name' => $magicword, 'aliases' => $aliases );
+ if ( $caseSensitive )
$arr['case-sensitive'] = '';
- $this->getResult()->setIndexedTagName($arr['aliases'], 'alias');
+ $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' );
$data[] = $arr;
}
- $this->getResult()->setIndexedTagName($data, 'magicword');
+ $this->getResult()->setIndexedTagName( $data, 'magicword' );
return $this->getResult()->addValue( 'query', $property, $data );
}
@@ -220,11 +240,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$this->addTables( 'interwiki' );
$this->addFields( array( 'iw_prefix', 'iw_local', 'iw_url' ) );
- if( $filter === 'local' )
+ if ( $filter === 'local' )
$this->addWhere( 'iw_local = 1' );
- elseif( $filter === '!local' )
+ elseif ( $filter === '!local' )
$this->addWhere( 'iw_local = 0' );
- elseif( $filter )
+ elseif ( $filter )
ApiBase :: dieDebug( __METHOD__, "Unknown filter=$filter" );
$this->addOption( 'ORDER BY', 'iw_prefix' );
@@ -234,14 +254,14 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data = array();
$langNames = Language::getLanguageNames();
- while( $row = $db->fetchObject($res) )
+ 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] ) )
+ if ( isset( $langNames[$row->iw_prefix] ) )
$val['language'] = $langNames[$row->iw_prefix];
$val['url'] = $row->iw_url;
@@ -256,13 +276,13 @@ class ApiQuerySiteinfo extends ApiQueryBase {
protected function appendDbReplLagInfo( $property, $includeAll ) {
global $wgShowHostnames;
$data = array();
- if( $includeAll ) {
+ if ( $includeAll ) {
if ( !$wgShowHostnames )
- $this->dieUsage('Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied');
+ $this->dieUsage( 'Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied' );
$lb = wfGetLB();
$lags = $lb->getLagTimes();
- foreach( $lags as $i => $lag ) {
+ foreach ( $lags as $i => $lag ) {
$data[] = array(
'host' => $lb->getServerName( $i ),
'lag' => $lag
@@ -293,16 +313,22 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$data['images'] = intval( SiteStats::images() );
$data['users'] = intval( SiteStats::users() );
$data['activeusers'] = intval( SiteStats::activeUsers() );
- $data['admins'] = intval( SiteStats::numberingroup('sysop') );
+ $data['admins'] = intval( SiteStats::numberingroup( 'sysop' ) );
$data['jobs'] = intval( SiteStats::jobs() );
return $this->getResult()->addValue( 'query', $property, $data );
}
- protected function appendUserGroups( $property ) {
+ protected function appendUserGroups( $property, $numberInGroup ) {
global $wgGroupPermissions;
$data = array();
- foreach( $wgGroupPermissions as $group => $permissions ) {
- $arr = array( 'name' => $group, 'rights' => array_keys( $permissions, true ) );
+ foreach ( $wgGroupPermissions as $group => $permissions ) {
+ $arr = array(
+ 'name' => $group,
+ 'rights' => array_keys( $permissions, true ),
+ );
+ if ( $numberInGroup )
+ $arr['number'] = SiteStats::numberInGroup( $group );
+
$this->getResult()->setIndexedTagName( $arr['rights'], 'permission' );
$data[] = $arr;
}
@@ -315,7 +341,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
global $wgFileExtensions;
$data = array();
- foreach( $wgFileExtensions as $ext ) {
+ foreach ( $wgFileExtensions as $ext ) {
$data[] = array( 'ext' => $ext );
}
$this->getResult()->setIndexedTagName( $data, 'fe' );
@@ -329,21 +355,32 @@ class ApiQuerySiteinfo extends ApiQueryBase {
foreach ( $extensions as $ext ) {
$ret = array();
$ret['type'] = $type;
- if ( isset( $ext['name'] ) )
+ if ( isset( $ext['name'] ) )
$ret['name'] = $ext['name'];
- if ( isset( $ext['description'] ) )
+ if ( isset( $ext['description'] ) )
$ret['description'] = $ext['description'];
- if ( isset( $ext['descriptionmsg'] ) )
- $ret['descriptionmsg'] = $ext['descriptionmsg'];
+ if ( isset( $ext['descriptionmsg'] ) ) {
+ // Can be a string or array( key, param1, param2, ... )
+ if ( is_array( $ext['descriptionmsg'] ) ) {
+ $ret['descriptionmsg'] = $ext['descriptionmsg'][0];
+ $ret['descriptionmsgparams'] = array_slice( $ext['descriptionmsg'], 1 );
+ $this->getResult()->setIndexedTagName( $ret['descriptionmsgparams'], 'param' );
+ } else {
+ $ret['descriptionmsg'] = $ext['descriptionmsg'];
+ }
+ }
if ( isset( $ext['author'] ) ) {
- $ret['author'] = is_array( $ext['author'] ) ?
+ $ret['author'] = is_array( $ext['author'] ) ?
implode( ', ', $ext['author' ] ) : $ext['author'];
}
+ if ( isset( $ext['url'] ) ) {
+ $ret['url'] = $ext['url'];
+ }
if ( isset( $ext['version'] ) ) {
$ret['version'] = $ext['version'];
- } elseif ( isset( $ext['svn-revision'] ) &&
- preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
- $ext['svn-revision'], $m ) )
+ } elseif ( isset( $ext['svn-revision'] ) &&
+ preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
+ $ext['svn-revision'], $m ) )
{
$ret['version'] = 'r' . $m[1];
}
@@ -361,7 +398,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
$title = Title::newFromText( $wgRightsPage );
$url = $title ? $title->getFullURL() : $wgRightsUrl;
$text = $wgRightsText;
- if( !$text && $title ) {
+ if ( !$text && $title ) {
$text = $title->getPrefixedText();
}
@@ -373,6 +410,17 @@ class ApiQuerySiteinfo extends ApiQueryBase {
return $this->getResult()->addValue( 'query', $property, $data );
}
+ public function appendLanguages( $property ) {
+ $data = array();
+ foreach ( Language::getLanguageNames() as $code => $name ) {
+ $lang = array( 'code' => $code );
+ ApiResult::setContent( $lang, $name );
+ $data[] = $lang;
+ }
+ $this->getResult()->setIndexedTagName( $data, 'lang' );
+ return $this->getResult()->addValue( 'query', $property, $data );
+ }
+
public function getCacheMode( $params ) {
return 'public';
}
@@ -395,6 +443,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
'extensions',
'fileextensions',
'rightsinfo',
+ 'languages',
)
),
'filteriw' => array(
@@ -404,6 +453,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
)
),
'showalldb' => false,
+ 'numberingroup' => false,
);
}
@@ -423,9 +473,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
' extensions - Returns extensions installed on the wiki',
' fileextensions - Returns list of file extensions allowed to be uploaded',
' rightsinfo - Returns wiki rights (license) information if available',
+ ' languages - Returns a list of languages MediaWiki supports',
),
'filteriw' => 'Return only local or only nonlocal entries of the interwiki map',
'showalldb' => 'List all database servers, not just the one lagging the most',
+ 'numberingroup' => 'Lists the number of users in user groups',
);
}
@@ -433,6 +485,12 @@ class ApiQuerySiteinfo extends ApiQueryBase {
return 'Return general information about the site.';
}
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'includeAllDenied', 'info' => 'Cannot view all servers info unless $wgShowHostnames is true' ),
+ ) );
+ }
+
protected function getExamples() {
return array(
'api.php?action=query&meta=siteinfo&siprop=general|namespaces|namespacealiases|statistics',
@@ -442,6 +500,6 @@ class ApiQuerySiteinfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php
new file mode 100644
index 00000000..a5d152bc
--- /dev/null
+++ b/includes/api/ApiQueryTags.php
@@ -0,0 +1,181 @@
+<?php
+
+/*
+ * Created on Jul 9, 2009
+ *
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2009
+ *
+ * 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 change tags.
+ *
+ * @ingroup API
+ */
+class ApiQueryTags extends ApiQueryBase {
+
+ private $limit, $result;
+ private $fld_displayname = false, $fld_description = false,
+ $fld_hitcount = false;
+
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'tg' );
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ $prop = array_flip( $params['prop'] );
+
+ $this->fld_displayname = isset( $prop['displayname'] );
+ $this->fld_description = isset( $prop['description'] );
+ $this->fld_hitcount = isset( $prop['hitcount'] );
+
+ $this->limit = $params['limit'];
+ $this->result = $this->getResult();
+
+ $pageSet = $this->getPageSet();
+ $titles = $pageSet->getTitles();
+ $data = array();
+
+ $this->addTables( 'change_tag' );
+ $this->addFields( 'ct_tag' );
+
+ if ( $this->fld_hitcount )
+ $this->addFields( 'count(*) AS hitcount' );
+
+ $this->addOption( 'LIMIT', $this->limit + 1 );
+ $this->addOption( 'GROUP BY', 'ct_tag' );
+ $this->addWhereRange( 'ct_tag', 'newer', $params['continue'], null );
+
+ $res = $this->select( __METHOD__ );
+
+ $ok = true;
+
+ while ( $row = $res->fetchObject() ) {
+ if ( !$ok ) break;
+ $ok = $this->doTag( $row->ct_tag, $row->hitcount );
+ }
+
+ // include tags with no hits yet
+ foreach ( ChangeTags::listDefinedTags() as $tag ) {
+ if ( !$ok ) break;
+ $ok = $this->doTag( $tag, 0 );
+ }
+
+ $this->result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'tag' );
+ }
+
+ private function doTag( $tagName, $hitcount ) {
+ static $count = 0;
+ static $doneTags = array();
+
+ if ( in_array( $tagName, $doneTags ) ) {
+ return true;
+ }
+
+ if ( ++$count > $this->limit )
+ {
+ $this->setContinueEnumParameter( 'continue', $tagName );
+ return false;
+ }
+
+ $tag = array();
+ $tag['name'] = $tagName;
+
+ if ( $this->fld_displayname )
+ $tag['displayname'] = ChangeTags::tagDescription( $tagName );
+
+ if ( $this->fld_description )
+ {
+ $msg = wfMsg( "tag-$tagName-description" );
+ $msg = wfEmptyMsg( "tag-$tagName-description", $msg ) ? '' : $msg;
+ $tag['description'] = $msg;
+ }
+
+ if ( $this->fld_hitcount )
+ $tag['hitcount'] = $hitcount;
+
+ $doneTags[] = $tagName;
+
+ $fit = $this->result->addValue( array( 'query', $this->getModuleName() ), null, $tag );
+ if ( !$fit )
+ {
+ $this->setContinueEnumParameter( 'continue', $tagName );
+ return false;
+ }
+
+ return true;
+ }
+
+ public function getCacheMode( $params ) {
+ return 'public';
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'continue' => 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
+ ),
+ 'prop' => array(
+ ApiBase :: PARAM_DFLT => 'name',
+ ApiBase :: PARAM_TYPE => array(
+ 'name',
+ 'displayname',
+ 'description',
+ 'hitcount'
+ ),
+ ApiBase :: PARAM_ISMULTI => true
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'continue' => 'When more results are available, use this to continue',
+ 'limit' => 'The maximum number of tags to list',
+ 'prop' => 'Which properties to get',
+ );
+ }
+
+ public function getDescription() {
+ return 'List change tags.';
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=query&list=tags&tgprop=displayname|description|hitcount'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiQueryTags.php 69932 2010-07-26 08:03:21Z tstarling $';
+ }
+}
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index 1c5cffa5..b51b9adb 100644
--- a/includes/api/ApiQueryUserContributions.php
+++ b/includes/api/ApiQueryUserContributions.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,33 +35,35 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryContributions extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'uc');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'uc' );
}
private $params, $username;
private $fld_ids = false, $fld_title = false, $fld_timestamp = false,
- $fld_comment = false, $fld_flags = false,
- $fld_patrolled = false;
+ $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false,
+ $fld_patrolled = false, $fld_tags = false;
public function execute() {
-
// Parse some parameters
$this->params = $this->extractRequestParams();
- $prop = array_flip($this->params['prop']);
- $this->fld_ids = isset($prop['ids']);
- $this->fld_title = isset($prop['title']);
- $this->fld_comment = isset($prop['comment']);
- $this->fld_flags = isset($prop['flags']);
- $this->fld_timestamp = isset($prop['timestamp']);
- $this->fld_patrolled = isset($prop['patrolled']);
+ $prop = array_flip( $this->params['prop'] );
+ $this->fld_ids = isset( $prop['ids'] );
+ $this->fld_title = isset( $prop['title'] );
+ $this->fld_comment = isset( $prop['comment'] );
+ $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_size = isset( $prop['size'] );
+ $this->fld_flags = isset( $prop['flags'] );
+ $this->fld_timestamp = isset( $prop['timestamp'] );
+ $this->fld_patrolled = isset( $prop['patrolled'] );
+ $this->fld_tags = isset( $prop['tags'] );
// TODO: if the query is going only against the revision table, should this be done?
- $this->selectNamedDB('contributions', DB_SLAVE, 'contributions');
+ $this->selectNamedDB( 'contributions', DB_SLAVE, 'contributions' );
$db = $this->getDB();
- if(isset($this->params['userprefix']))
+ if ( isset( $this->params['userprefix'] ) )
{
$this->prefixMode = true;
$this->multiUserMode = true;
@@ -70,61 +72,63 @@ class ApiQueryContributions extends ApiQueryBase {
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);
+ if ( !is_array( $this->params['user'] ) )
+ $this->params['user'] = array( $this->params['user'] );
+ if ( !count( $this->params['user'] ) )
+ $this->dieUsage( 'User parameter may not be empty.', 'param_user' );
+ foreach ( $this->params['user'] as $u )
+ $this->prepareUsername( $u );
$this->prefixMode = false;
- $this->multiUserMode = (count($this->params['user']) > 1);
+ $this->multiUserMode = ( count( $this->params['user'] ) > 1 );
}
$this->prepareQuery();
- //Do the actual query.
+ // Do the actual query.
$res = $this->select( __METHOD__ );
- //Initialise some variables
+ // Initialise some variables
$count = 0;
$limit = $this->params['limit'];
- //Fetch each row
+ // Fetch each row
while ( $row = $db->fetchObject( $res ) ) {
- if (++ $count > $limit) {
+ if ( ++ $count > $limit ) {
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- if($this->multiUserMode)
- $this->setContinueEnumParameter('continue', $this->continueStr($row));
+ if ( $this->multiUserMode )
+ $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
else
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rev_timestamp ) );
break;
}
- $vals = $this->extractRowInfo($row);
- $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ $vals = $this->extractRowInfo( $row );
+ $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- if($this->multiUserMode)
- $this->setContinueEnumParameter('continue', $this->continueStr($row));
+ if ( $this->multiUserMode )
+ $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) );
else
- $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rev_timestamp ) );
break;
}
}
- //Free the database record so the connection can get on with other stuff
- $db->freeResult($res);
+ // Free the database record so the connection can get on with other stuff
+ $db->freeResult( $res );
- $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item');
+ $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' );
}
/**
* Validate the 'user' parameter and set the value to compare
* against `revision`.`rev_user_text`
*/
- private function prepareUsername($user) {
- if( $user ) {
+ private function prepareUsername( $user ) {
+ if ( !is_null( $user ) && $user !== '' ) {
$name = User::isIP( $user )
? $user
: User::getCanonicalName( $user, 'valid' );
- if( $name === false ) {
+ if ( $name === false ) {
$this->dieUsage( "User name {$user} is not valid", 'param_user' );
} else {
$this->usernames[] = $name;
@@ -140,155 +144,202 @@ class ApiQueryContributions extends ApiQueryBase {
private function prepareQuery() {
// We're after the revision table, and the corresponding page
// row for anything we retrieve. We may also need the
- // recentchanges row.
- $tables = array('page', 'revision'); // Order may change
- $this->addWhere('page_id=rev_page');
+ // recentchanges row and/or tag summary row.
+ global $wgUser;
+ $tables = array( 'page', 'revision' ); // Order may change
+ $this->addWhere( 'page_id=rev_page' );
// Handle continue parameter
- if($this->multiUserMode && !is_null($this->params['continue']))
+ if ( $this->multiUserMode && !is_null( $this->params['continue'] ) )
{
- $continue = explode('|', $this->params['continue']);
- if(count($continue) != 2)
- $this->dieUsage("Invalid continue param. You should pass the original " .
- "value returned by the previous query", "_badcontinue");
- $encUser = $this->getDB()->strencode($continue[0]);
- $encTS = wfTimestamp(TS_MW, $continue[1]);
- $op = ($this->params['dir'] == 'older' ? '<' : '>');
- $this->addWhere("rev_user_text $op '$encUser' OR " .
+ $continue = explode( '|', $this->params['continue'] );
+ if ( count( $continue ) != 2 )
+ $this->dieUsage( "Invalid continue param. You should pass the original " .
+ "value returned by the previous query", "_badcontinue" );
+ $encUser = $this->getDB()->strencode( $continue[0] );
+ $encTS = wfTimestamp( TS_MW, $continue[1] );
+ $op = ( $this->params['dir'] == 'older' ? '<' : '>' );
+ $this->addWhere( "rev_user_text $op '$encUser' OR " .
"(rev_user_text = '$encUser' AND " .
- "rev_timestamp $op= '$encTS')");
+ "rev_timestamp $op= '$encTS')" );
}
- $this->addWhereFld('rev_deleted', 0);
+ if ( !$wgUser->isAllowed( 'hideuser' ) )
+ $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
// We only want pages by the specified users.
- if($this->prefixMode)
- $this->addWhere("rev_user_text LIKE '" . $this->getDB()->escapeLike($this->userprefix) . "%'");
+ if ( $this->prefixMode )
+ $this->addWhere( 'rev_user_text' . $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) );
else
- $this->addWhereFld('rev_user_text', $this->usernames);
+ $this->addWhereFld( 'rev_user_text', $this->usernames );
// ... and in the specified timeframe.
// Ensure the same sort order for rev_user_text and rev_timestamp
// so our query is indexed
- if($this->multiUserMode)
- $this->addWhereRange('rev_user_text', $this->params['dir'], null, null);
- $this->addWhereRange('rev_timestamp',
+ if ( $this->multiUserMode )
+ $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']);
+ $this->addWhereFld( 'page_namespace', $this->params['namespace'] );
$show = $this->params['show'];
- if (!is_null($show)) {
- $show = array_flip($show);
- if ((isset($show['minor']) && isset($show['!minor']))
- || (isset($show['patrolled']) && isset($show['!patrolled'])))
- $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
-
- $this->addWhereIf('rev_minor_edit = 0', isset($show['!minor']));
- $this->addWhereIf('rev_minor_edit != 0', isset($show['minor']));
- $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled']));
- $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled']));
+ if ( !is_null( $show ) ) {
+ $show = array_flip( $show );
+ if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) )
+ || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) )
+ $this->dieUsageMsg( array( 'show' ) );
+
+ $this->addWhereIf( 'rev_minor_edit = 0', isset( $show['!minor'] ) );
+ $this->addWhereIf( 'rev_minor_edit != 0', isset( $show['minor'] ) );
+ $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) );
+ $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) );
}
- $this->addOption('LIMIT', $this->params['limit'] + 1);
- $index['revision'] = 'usertext_timestamp';
+ $this->addOption( 'LIMIT', $this->params['limit'] + 1 );
+ $index = array( 'revision' => 'usertext_timestamp' );
// 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
- $this->addFields(array(
+ $this->addFields( array(
'rev_timestamp',
'page_namespace',
'page_title',
'rev_user_text',
- ));
+ 'rev_deleted'
+ ) );
- if(isset($show['patrolled']) || isset($show['!patrolled']) ||
- $this->fld_patrolled)
+ if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ||
+ $this->fld_patrolled )
{
global $wgUser;
- if(!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
- $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
+ if ( !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol() )
+ $this->dieUsage( "You need the patrol right to request the patrolled flag", 'permissiondenied' );
// Use a redundant join condition on both
// timestamp and ID so we can use the timestamp
// index
$index['recentchanges'] = 'rc_user_text';
- if(isset($show['patrolled']) || isset($show['!patrolled']))
+ if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) )
{
// Put the tables in the right order for
// STRAIGHT_JOIN
- $tables = array('revision', 'recentchanges', 'page');
- $this->addOption('STRAIGHT_JOIN');
- $this->addWhere('rc_user_text=rev_user_text');
- $this->addWhere('rc_timestamp=rev_timestamp');
- $this->addWhere('rc_this_oldid=rev_id');
+ $tables = array( 'revision', 'recentchanges', 'page' );
+ $this->addOption( 'STRAIGHT_JOIN' );
+ $this->addWhere( 'rc_user_text=rev_user_text' );
+ $this->addWhere( 'rc_timestamp=rev_timestamp' );
+ $this->addWhere( 'rc_this_oldid=rev_id' );
}
else
{
$tables[] = 'recentchanges';
- $this->addJoinConds(array('recentchanges' => array(
+ $this->addJoinConds( array( 'recentchanges' => array(
'LEFT JOIN', array(
'rc_user_text=rev_user_text',
'rc_timestamp=rev_timestamp',
- 'rc_this_oldid=rev_id'))));
+ 'rc_this_oldid=rev_id' ) ) ) );
}
}
- $this->addTables($tables);
- $this->addOption('USE INDEX', $index);
- $this->addFieldsIf('rev_page', $this->fld_ids);
- $this->addFieldsIf('rev_id', $this->fld_ids || $this->fld_flags);
- $this->addFieldsIf('page_latest', $this->fld_flags);
+ $this->addTables( $tables );
+ $this->addFieldsIf( 'rev_page', $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('rev_parent_id', $this->fld_flags);
- $this->addFieldsIf('rc_patrolled', $this->fld_patrolled);
+ $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment );
+ $this->addFieldsIf( 'rev_len', $this->fld_size );
+ $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags );
+ $this->addFieldsIf( 'rev_parent_id', $this->fld_flags );
+ $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled );
+
+ if ( $this->fld_tags )
+ {
+ $this->addTables( 'tag_summary' );
+ $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) );
+ $this->addFields( 'ts_tags' );
+ }
+
+ if ( isset( $this->params['tag'] ) ) {
+ $this->addTables( 'change_tag' );
+ $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) );
+ $this->addWhereFld( 'ct_tag', $this->params['tag'] );
+ global $wgOldChangeTagsIndex;
+ $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
+ }
+
+ $this->addOption( 'USE INDEX', $index );
}
/**
* Extract fields from the database row and append them to a result array
*/
- private function extractRowInfo($row) {
+ private function extractRowInfo( $row ) {
$vals = array();
$vals['user'] = $row->rev_user_text;
- if ($this->fld_ids) {
- $vals['pageid'] = intval($row->rev_page);
- $vals['revid'] = intval($row->rev_id);
+ if ( $row->rev_deleted & Revision::DELETED_USER )
+ $vals['userhidden'] = '';
+ if ( $this->fld_ids ) {
+ $vals['pageid'] = intval( $row->rev_page );
+ $vals['revid'] = intval( $row->rev_id );
// $vals['textid'] = intval($row->rev_text_id); // todo: Should this field be exposed?
}
- if ($this->fld_title)
- ApiQueryBase :: addTitleInfo($vals,
- Title :: makeTitle($row->page_namespace, $row->page_title));
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
- if ($this->fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
+ if ( $this->fld_title )
+ ApiQueryBase::addTitleInfo( $vals, $title );
- if ($this->fld_flags) {
- if ($row->rev_parent_id == 0)
+ if ( $this->fld_timestamp )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
+
+ if ( $this->fld_flags ) {
+ if ( $row->rev_parent_id == 0 && !is_null( $row->rev_parent_id ) )
$vals['new'] = '';
- if ($row->rev_minor_edit)
+ if ( $row->rev_minor_edit )
$vals['minor'] = '';
- if ($row->page_latest == $row->rev_id)
+ if ( $row->page_latest == $row->rev_id )
$vals['top'] = '';
}
- if ($this->fld_comment && isset($row->rev_comment))
- $vals['comment'] = $row->rev_comment;
+ if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) {
+ if ( $row->rev_deleted & Revision::DELETED_COMMENT )
+ $vals['commenthidden'] = '';
+ else {
+ if ( $this->fld_comment )
+ $vals['comment'] = $row->rev_comment;
+
+ if ( $this->fld_parsedcomment ) {
+ global $wgUser;
+ $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->rev_comment, $title );
+ }
+ }
+ }
- if ($this->fld_patrolled && $row->rc_patrolled)
+ if ( $this->fld_patrolled && $row->rc_patrolled )
$vals['patrolled'] = '';
-
+
+ if ( $this->fld_size && !is_null( $row->rev_len ) )
+ $vals['size'] = intval( $row->rev_len );
+
+ if ( $this->fld_tags ) {
+ if ( $row->ts_tags ) {
+ $tags = explode( ',', $row->ts_tags );
+ $this->getResult()->setIndexedTagName( $tags, 'tag' );
+ $vals['tags'] = $tags;
+ } else {
+ $vals['tags'] = array();
+ }
+ }
+
return $vals;
}
- private function continueStr($row)
+ private function continueStr( $row )
{
return $row->rev_user_text . '|' .
- wfTimestamp(TS_ISO_8601, $row->rev_timestamp);
+ wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
}
public function getCacheMode( $params ) {
- // This module provides access to patrol flags if
+ // This module provides access to deleted revisions and patrol flags if
// the requester is logged in
return 'anon-public-user-private';
}
@@ -326,14 +377,17 @@ class ApiQueryContributions extends ApiQueryBase {
),
'prop' => array (
ApiBase :: PARAM_ISMULTI => true,
- ApiBase :: PARAM_DFLT => 'ids|title|timestamp|flags|comment',
+ ApiBase :: PARAM_DFLT => 'ids|title|timestamp|comment|size|flags',
ApiBase :: PARAM_TYPE => array (
'ids',
'title',
'timestamp',
'comment',
+ 'parsedcomment',
+ 'size',
'flags',
'patrolled',
+ 'tags'
)
),
'show' => array (
@@ -345,6 +399,7 @@ class ApiQueryContributions extends ApiQueryBase {
'!patrolled',
)
),
+ 'tag' => null,
);
}
@@ -359,14 +414,24 @@ class ApiQueryContributions extends ApiQueryBase {
'dir' => 'The direction to search (older or newer).',
'namespace' => 'Only list contributions in these namespaces',
'prop' => 'Include additional pieces of information',
- 'show' => array('Show only items that meet this criteria, e.g. non minor edits only: show=!minor',
- 'NOTE: if show=patrolled or show=!patrolled is set, revisions older than $wgRCMaxAge won\'t be shown',),
+ 'show' => array( 'Show only items that meet this criteria, e.g. non minor edits only: show=!minor',
+ 'NOTE: if show=patrolled or show=!patrolled is set, revisions older than $wgRCMaxAge won\'t be shown', ),
+ 'tag' => 'Only list revisions tagged with this tag',
);
}
public function getDescription() {
return 'Get all edits by a user';
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'param_user', 'info' => 'User parameter may not be empty.' ),
+ array( 'code' => 'param_user', 'info' => 'User name user is not valid' ),
+ array( 'show' ),
+ array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -376,6 +441,6 @@ class ApiQueryContributions extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserContributions.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryUserContributions.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index e445c46e..42cb47b9 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryUserInfo extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'ui');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'ui' );
}
public function execute() {
@@ -44,59 +44,76 @@ class ApiQueryUserInfo extends ApiQueryBase {
$result = $this->getResult();
$r = array();
- if (!is_null($params['prop'])) {
- $this->prop = array_flip($params['prop']);
+ if ( !is_null( $params['prop'] ) ) {
+ $this->prop = array_flip( $params['prop'] );
} else {
$this->prop = array();
}
$r = $this->getCurrentUserInfo();
- $result->addValue("query", $this->getModuleName(), $r);
+ $result->addValue( "query", $this->getModuleName(), $r );
}
protected function getCurrentUserInfo() {
global $wgUser;
$result = $this->getResult();
$vals = array();
- $vals['id'] = intval($wgUser->getId());
+ $vals['id'] = intval( $wgUser->getId() );
$vals['name'] = $wgUser->getName();
- if($wgUser->isAnon())
+ if ( $wgUser->isAnon() )
$vals['anon'] = '';
- if (isset($this->prop['blockinfo'])) {
- if ($wgUser->isBlocked()) {
- $vals['blockedby'] = User::whoIs($wgUser->blockedBy());
+
+ if ( isset( $this->prop['blockinfo'] ) ) {
+ if ( $wgUser->isBlocked() ) {
+ $vals['blockedby'] = User::whoIs( $wgUser->blockedBy() );
$vals['blockreason'] = $wgUser->blockedFor();
}
}
- if (isset($this->prop['hasmsg']) && $wgUser->getNewtalk()) {
+
+ if ( isset( $this->prop['hasmsg'] ) && $wgUser->getNewtalk() ) {
$vals['messages'] = '';
}
- if (isset($this->prop['groups'])) {
+
+ if ( isset( $this->prop['groups'] ) ) {
$vals['groups'] = $wgUser->getGroups();
- $result->setIndexedTagName($vals['groups'], 'g'); // even if empty
+ $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty
}
- if (isset($this->prop['rights'])) {
+
+ if ( isset( $this->prop['rights'] ) ) {
// User::getRights() may return duplicate values, strip them
- $vals['rights'] = array_values(array_unique($wgUser->getRights()));
- $result->setIndexedTagName($vals['rights'], 'r'); // even if empty
+ $vals['rights'] = array_values( array_unique( $wgUser->getRights() ) );
+ $result->setIndexedTagName( $vals['rights'], 'r' ); // even if empty
}
- if (isset($this->prop['options'])) {
- $vals['options'] = (is_null($wgUser->mOptions) ? User::getDefaultOptions() : $wgUser->mOptions);
+
+ if ( isset( $this->prop['changeablegroups'] ) ) {
+ $vals['changeablegroups'] = $wgUser->changeableGroups();
+ $result->setIndexedTagName( $vals['changeablegroups']['add'], 'g' );
+ $result->setIndexedTagName( $vals['changeablegroups']['remove'], 'g' );
+ $result->setIndexedTagName( $vals['changeablegroups']['add-self'], 'g' );
+ $result->setIndexedTagName( $vals['changeablegroups']['remove-self'], 'g' );
}
- if (isset($this->prop['preferencestoken']) && is_null($this->getMain()->getRequest()->getVal('callback'))) {
+
+ if ( isset( $this->prop['options'] ) ) {
+ $vals['options'] = $wgUser->getOptions();
+ }
+
+ if ( isset( $this->prop['preferencestoken'] ) && is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) {
$vals['preferencestoken'] = $wgUser->editToken();
}
- if (isset($this->prop['editcount'])) {
- $vals['editcount'] = intval($wgUser->getEditCount());
+
+ if ( isset( $this->prop['editcount'] ) ) {
+ $vals['editcount'] = intval( $wgUser->getEditCount() );
}
- if (isset($this->prop['ratelimits'])) {
+
+ if ( isset( $this->prop['ratelimits'] ) ) {
$vals['ratelimits'] = $this->getRateLimits();
}
- if (isset($this->prop['email'])) {
+
+ if ( isset( $this->prop['email'] ) ) {
$vals['email'] = $wgUser->getEmail();
$auth = $wgUser->getEmailAuthenticationTimestamp();
- if(!is_null($auth))
- $vals['emailauthenticated'] = wfTimestamp(TS_ISO_8601, $auth);
+ if ( !is_null( $auth ) )
+ $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth );
}
return $vals;
}
@@ -104,32 +121,32 @@ class ApiQueryUserInfo extends ApiQueryBase {
protected function getRateLimits()
{
global $wgUser, $wgRateLimits;
- if(!$wgUser->isPingLimitable())
+ if ( !$wgUser->isPingLimitable() )
return array(); // No limits
// Find out which categories we belong to
$categories = array();
- if($wgUser->isAnon())
+ if ( $wgUser->isAnon() )
$categories[] = 'anon';
else
$categories[] = 'user';
- if($wgUser->isNewBie())
+ if ( $wgUser->isNewBie() )
{
$categories[] = 'ip';
$categories[] = 'subnet';
- if(!$wgUser->isAnon())
+ if ( !$wgUser->isAnon() )
$categories[] = 'newbie';
}
- $categories = array_merge($categories, $wgUser->getGroups());
+ $categories = array_merge( $categories, $wgUser->getGroups() );
// Now get the actual limits
$retval = array();
- foreach($wgRateLimits as $action => $limits)
- foreach($categories as $cat)
- if(isset($limits[$cat]) && !is_null($limits[$cat]))
+ foreach ( $wgRateLimits as $action => $limits )
+ foreach ( $categories as $cat )
+ if ( isset( $limits[$cat] ) && !is_null( $limits[$cat] ) )
{
- $retval[$action][$cat]['hits'] = intval($limits[$cat][0]);
- $retval[$action][$cat]['seconds'] = intval($limits[$cat][1]);
+ $retval[$action][$cat]['hits'] = intval( $limits[$cat][0] );
+ $retval[$action][$cat]['seconds'] = intval( $limits[$cat][1] );
}
return $retval;
}
@@ -137,13 +154,14 @@ class ApiQueryUserInfo extends ApiQueryBase {
public function getAllowedParams() {
return array (
'prop' => array (
- ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_DFLT => null,
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
'blockinfo',
'hasmsg',
'groups',
'rights',
+ 'changeablegroups',
'options',
'preferencestoken',
'editcount',
@@ -161,7 +179,8 @@ class ApiQueryUserInfo extends ApiQueryBase {
' blockinfo - tags if the current user is blocked, by whom, and for what reason',
' hasmsg - adds a tag "message" if the current user has pending messages',
' groups - lists all the groups the current user belongs to',
- ' rights - lists of all rights the current user has',
+ ' rights - lists all the rights the current user has',
+ ' changeablegroups - lists the groups the current user can add to and remove from',
' options - lists all preferences the current user has set',
' editcount - adds the current user\'s edit count',
' ratelimits - lists all rate limits applying to the current user'
@@ -181,6 +200,6 @@ class ApiQueryUserInfo extends ApiQueryBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUserInfo.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryUserInfo.php 69578 2010-07-20 02:46:20Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index 1e50c59a..5dc0e4a6 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -33,11 +33,40 @@ if (!defined('MEDIAWIKI')) {
*
* @ingroup API
*/
-
class ApiQueryUsers extends ApiQueryBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'us');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'us' );
+ }
+
+ /**
+ * Get an array mapping token names to their handler functions.
+ * The prototype for a token function is func($user)
+ * it should return a token or false (permission denied)
+ * @return array(tokenname => function)
+ */
+ protected function getTokenFunctions() {
+ // 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(
+ 'userrights' => array( 'ApiQueryUsers', 'getUserrightsToken' ),
+ );
+ wfRunHooks( 'APIQueryUsersTokens', array( &$this->tokenFunctions ) );
+ return $this->tokenFunctions;
+ }
+
+ public static function getUserrightsToken( $user )
+ {
+ global $wgUser;
+ // Since the permissions check for userrights is non-trivial,
+ // don't bother with it here
+ return $wgUser->editToken( $user->getName() );
}
public function execute() {
@@ -45,8 +74,8 @@ if (!defined('MEDIAWIKI')) {
$result = $this->getResult();
$r = array();
- if (!is_null($params['prop'])) {
- $this->prop = array_flip($params['prop']);
+ if ( !is_null( $params['prop'] ) ) {
+ $this->prop = array_flip( $params['prop'] );
} else {
$this->prop = array();
}
@@ -55,17 +84,17 @@ if (!defined('MEDIAWIKI')) {
$goodNames = $done = array();
$result = $this->getResult();
// Canonicalize user names
- foreach($users as $u) {
- $n = User::getCanonicalName($u);
- if($n === false || $n === '')
+ foreach ( $users as $u ) {
+ $n = User::getCanonicalName( $u );
+ if ( $n === false || $n === '' )
{
- $vals = array('name' => $u, 'invalid' => '');
- $fit = $result->addValue(array('query', $this->getModuleName()),
- null, $vals);
- if(!$fit)
+ $vals = array( 'name' => $u, 'invalid' => '' );
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ),
+ null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('users',
- implode('|', array_diff($users, $done)));
+ $this->setContinueEnumParameter( 'users',
+ implode( '|', array_diff( $users, $done ) ) );
$goodNames = array();
break;
}
@@ -74,78 +103,122 @@ if (!defined('MEDIAWIKI')) {
else
$goodNames[] = $n;
}
- if(count($goodNames))
+
+ if ( count( $goodNames ) )
{
$db = $this->getDb();
- $this->addTables('user', 'u1');
- $this->addFields('u1.*');
- $this->addWhereFld('u1.user_name', $goodNames);
-
- if(isset($this->prop['groups'])) {
- $this->addTables('user_groups');
- $this->addJoinConds(array('user_groups' => array('LEFT JOIN', 'ug_user=u1.user_id')));
- $this->addFields('ug_group');
+ $this->addTables( 'user', 'u1' );
+ $this->addFields( 'u1.*' );
+ $this->addWhereFld( 'u1.user_name', $goodNames );
+
+ if ( isset( $this->prop['groups'] ) ) {
+ $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'])) {
- $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 AS blocker_name'));
+ if ( isset( $this->prop['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 AS blocker_name' ) );
}
$data = array();
- $res = $this->select(__METHOD__);
- while(($r = $db->fetchObject($res))) {
- $user = User::newFromRow($r);
+ $res = $this->select( __METHOD__ );
+ while ( ( $r = $db->fetchObject( $res ) ) ) {
+ $user = User::newFromRow( $r );
$name = $user->getName();
$data[$name]['name'] = $name;
- if(isset($this->prop['editcount']))
- $data[$name]['editcount'] = intval($user->getEditCount());
- if(isset($this->prop['registration']))
- $data[$name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $user->getRegistration());
- if(isset($this->prop['groups']) && !is_null($r->ug_group))
+ if ( isset( $this->prop['editcount'] ) )
+ $data[$name]['editcount'] = intval( $user->getEditCount() );
+ if ( isset( $this->prop['registration'] ) )
+ $data[$name]['registration'] = wfTimestampOrNull( TS_ISO_8601, $user->getRegistration() );
+ if ( isset( $this->prop['groups'] ) && !is_null( $r->ug_group ) )
// This row contains only one group, others will be added from other rows
$data[$name]['groups'][] = $r->ug_group;
- if(isset($this->prop['blockinfo']) && !is_null($r->blocker_name)) {
+ if ( isset( $this->prop['blockinfo'] ) && !is_null( $r->blocker_name ) ) {
$data[$name]['blockedby'] = $r->blocker_name;
$data[$name]['blockreason'] = $r->ipb_reason;
}
- if(isset($this->prop['emailable']) && $user->canReceiveEmail())
+ if ( isset( $this->prop['emailable'] ) && $user->canReceiveEmail() )
$data[$name]['emailable'] = '';
+
+ if ( isset( $this->prop['gender'] ) ) {
+ $gender = $user->getOption( 'gender' );
+ if ( strval( $gender ) === '' ) {
+ $gender = 'unknown';
+ }
+ $data[$name]['gender'] = $gender;
+ }
+
+ if ( !is_null( $params['token'] ) )
+ {
+ $tokenFunctions = $this->getTokenFunctions();
+ foreach ( $params['token'] as $t )
+ {
+ $val = call_user_func( $tokenFunctions[$t], $user );
+ if ( $val === false )
+ $this->setWarning( "Action '$t' is not allowed for the current user" );
+ else
+ $data[$name][$t . 'token'] = $val;
+ }
+ }
}
}
// Second pass: add result data to $retval
- foreach($goodNames as $u) {
- if(!isset($data[$u]))
- $data[$u] = array('name' => $u, 'missing' => '');
- else {
- if(isset($this->prop['groups']) && isset($data[$u]['groups']))
- $this->getResult()->setIndexedTagName($data[$u]['groups'], 'g');
+ foreach ( $goodNames as $u ) {
+ if ( !isset( $data[$u] ) ) {
+ $data[$u] = array( 'name' => $u );
+ $urPage = new UserrightsPage;
+ $iwUser = $urPage->fetchUser( $u );
+ if ( $iwUser instanceof UserRightsProxy ) {
+ $data[$u]['interwiki'] = '';
+ if ( !is_null( $params['token'] ) )
+ {
+ $tokenFunctions = $this->getTokenFunctions();
+ foreach ( $params['token'] as $t )
+ {
+ $val = call_user_func( $tokenFunctions[$t], $iwUser );
+ if ( $val === false )
+ $this->setWarning( "Action '$t' is not allowed for the current user" );
+ else
+ $data[$u][$t . 'token'] = $val;
+ }
+ }
+ } else
+ $data[$u]['missing'] = '';
+ } else {
+ if ( isset( $this->prop['groups'] ) && isset( $data[$u]['groups'] ) )
+ $this->getResult()->setIndexedTagName( $data[$u]['groups'], 'g' );
}
- $fit = $result->addValue(array('query', $this->getModuleName()),
- null, $data[$u]);
- if(!$fit)
+ $fit = $result->addValue( array( 'query', $this->getModuleName() ),
+ null, $data[$u] );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('users',
- implode('|', array_diff($users, $done)));
+ $this->setContinueEnumParameter( 'users',
+ implode( '|', array_diff( $users, $done ) ) );
break;
}
$done[] = $u;
}
- return $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'user');
+ return $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'user' );
}
public function getCacheMode( $params ) {
- return 'public';
+ if ( isset( $params['token'] ) ) {
+ return 'private';
+ } else {
+ return 'public';
+ }
}
public function getAllowedParams() {
return array (
'prop' => array (
- ApiBase :: PARAM_DFLT => NULL,
+ ApiBase :: PARAM_DFLT => null,
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => array (
'blockinfo',
@@ -153,11 +226,16 @@ if (!defined('MEDIAWIKI')) {
'editcount',
'registration',
'emailable',
+ 'gender',
)
),
'users' => array(
ApiBase :: PARAM_ISMULTI => true
- )
+ ),
+ 'token' => array(
+ ApiBase :: PARAM_TYPE => array_keys( $this->getTokenFunctions() ),
+ ApiBase :: PARAM_ISMULTI => true
+ ),
);
}
@@ -170,8 +248,10 @@ if (!defined('MEDIAWIKI')) {
' editcount - adds the user\'s edit count',
' registration - adds the user\'s registration timestamp',
' emailable - tags if the user can and wants to receive e-mail through [[Special:Emailuser]]',
+ ' gender - tags the gender of the user. Returns "male", "female", or "unknown"',
),
- 'users' => 'A list of users to obtain the same information for'
+ 'users' => 'A list of users to obtain the same information for',
+ 'token' => 'Which tokens to obtain for each user',
);
}
@@ -180,10 +260,10 @@ if (!defined('MEDIAWIKI')) {
}
protected function getExamples() {
- return 'api.php?action=query&list=users&ususers=brion|TimStarling&usprop=groups|editcount';
+ return 'api.php?action=query&list=users&ususers=brion|TimStarling&usprop=groups|editcount|gender';
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryUsers.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryUsers.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index eb5c531f..caac0706 100644
--- a/includes/api/ApiQueryWatchlist.php
+++ b/includes/api/ApiQueryWatchlist.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -36,221 +36,244 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryWatchlist extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'wl');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'wl' );
}
public function execute() {
$this->run();
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
private $fld_ids = false, $fld_title = false, $fld_patrol = false, $fld_flags = false,
- $fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_sizes = false;
+ $fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false,
+ $fld_notificationtimestamp = false;
- private function run($resultPageSet = null) {
- global $wgUser, $wgDBtype;
+ private function run( $resultPageSet = null ) {
+ global $wgUser;
- $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist');
-
- if (!$wgUser->isLoggedIn())
- $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
+ $this->selectNamedDB( 'watchlist', DB_SLAVE, 'watchlist' );
$params = $this->extractRequestParams();
- if (!is_null($params['prop']) && is_null($resultPageSet)) {
+ if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) {
+ $user = User::newFromName( $params['owner'], false );
+ if ( !$user->getId() ) {
+ $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' );
+ }
+ $token = $user->getOption( 'watchlisttoken' );
+ if ( $token == '' || $token != $params['token'] ) {
+ $this->dieUsage( 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', 'bad_wltoken' );
+ }
+ } elseif ( !$wgUser->isLoggedIn() ) {
+ $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
+ } else {
+ $user = $wgUser;
+ }
+
+ if ( !is_null( $params['prop'] ) && is_null( $resultPageSet ) ) {
- $prop = array_flip($params['prop']);
+ $prop = array_flip( $params['prop'] );
- $this->fld_ids = isset($prop['ids']);
- $this->fld_title = isset($prop['title']);
- $this->fld_flags = isset($prop['flags']);
- $this->fld_user = isset($prop['user']);
- $this->fld_comment = isset($prop['comment']);
- $this->fld_timestamp = isset($prop['timestamp']);
- $this->fld_sizes = isset($prop['sizes']);
- $this->fld_patrol = isset($prop['patrol']);
+ $this->fld_ids = isset( $prop['ids'] );
+ $this->fld_title = isset( $prop['title'] );
+ $this->fld_flags = isset( $prop['flags'] );
+ $this->fld_user = isset( $prop['user'] );
+ $this->fld_comment = isset( $prop['comment'] );
+ $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
+ $this->fld_timestamp = isset( $prop['timestamp'] );
+ $this->fld_sizes = isset( $prop['sizes'] );
+ $this->fld_patrol = isset( $prop['patrol'] );
+ $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
- if ($this->fld_patrol) {
- global $wgUser;
- if (!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
- $this->dieUsage('patrol property is not available', 'patrol');
+ if ( $this->fld_patrol ) {
+ if ( !$user->useRCPatrol() && !$user->useNPPatrol() )
+ $this->dieUsage( 'patrol property is not available', 'patrol' );
}
}
-
- if (is_null($resultPageSet)) {
- $this->addFields(array (
+
+ $this->addFields( array (
+ 'rc_namespace',
+ 'rc_title',
+ 'rc_timestamp'
+ ) );
+
+ if ( is_null( $resultPageSet ) ) {
+ $this->addFields( array (
'rc_cur_id',
- 'rc_this_oldid',
- 'rc_namespace',
- 'rc_title',
- 'rc_timestamp'
- ));
-
- $this->addFieldsIf('rc_new', $this->fld_flags);
- $this->addFieldsIf('rc_minor', $this->fld_flags);
- $this->addFieldsIf('rc_bot', $this->fld_flags);
- $this->addFieldsIf('rc_user', $this->fld_user);
- $this->addFieldsIf('rc_user_text', $this->fld_user);
- $this->addFieldsIf('rc_comment', $this->fld_comment);
- $this->addFieldsIf('rc_patrolled', $this->fld_patrol);
- $this->addFieldsIf('rc_old_len', $this->fld_sizes);
- $this->addFieldsIf('rc_new_len', $this->fld_sizes);
- }
- elseif ($params['allrev']) {
- $this->addFields(array (
- 'rc_this_oldid',
- 'rc_namespace',
- 'rc_title',
- 'rc_timestamp'
- ));
+ 'rc_this_oldid'
+ ) );
+
+ $this->addFieldsIf( 'rc_new', $this->fld_flags );
+ $this->addFieldsIf( 'rc_minor', $this->fld_flags );
+ $this->addFieldsIf( 'rc_bot', $this->fld_flags );
+ $this->addFieldsIf( 'rc_user', $this->fld_user );
+ $this->addFieldsIf( 'rc_user_text', $this->fld_user );
+ $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment );
+ $this->addFieldsIf( 'rc_patrolled', $this->fld_patrol );
+ $this->addFieldsIf( 'rc_old_len', $this->fld_sizes );
+ $this->addFieldsIf( 'rc_new_len', $this->fld_sizes );
+ $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp );
+ } elseif ( $params['allrev'] ) {
+ $this->addFields( 'rc_this_oldid' );
} else {
- $this->addFields(array (
- 'rc_cur_id',
- 'rc_namespace',
- 'rc_title',
- 'rc_timestamp'
- ));
+ $this->addFields( 'rc_cur_id' );
}
- $this->addTables(array (
+ $this->addTables( array (
'watchlist',
'page',
'recentchanges'
- ));
+ ) );
- $userId = $wgUser->getId();
- $this->addWhere(array (
+ $userId = $user->getId();
+ $this->addWhere( array (
'wl_namespace = rc_namespace',
'wl_title = rc_title',
'rc_cur_id = page_id',
'wl_user' => $userId,
'rc_deleted' => 0,
- ));
+ ) );
- $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']);
- $this->addWhereFld('wl_namespace', $params['namespace']);
- $this->addWhereIf('rc_this_oldid=page_latest', !$params['allrev']);
+ $this->addWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] );
+ $this->addWhereFld( 'wl_namespace', $params['namespace'] );
+ $this->addWhereIf( 'rc_this_oldid=page_latest', !$params['allrev'] );
- if (!is_null($params['show'])) {
- $show = array_flip($params['show']);
+ if ( !is_null( $params['show'] ) ) {
+ $show = array_flip( $params['show'] );
/* Check for conflicting parameters. */
- if ((isset ($show['minor']) && isset ($show['!minor']))
- || (isset ($show['bot']) && isset ($show['!bot']))
- || (isset ($show['anon']) && isset ($show['!anon']))
- || (isset ($show['patrolled']) && isset ($show['!patrolled']))) {
+ if ( ( isset ( $show['minor'] ) && isset ( $show['!minor'] ) )
+ || ( isset ( $show['bot'] ) && isset ( $show['!bot'] ) )
+ || ( isset ( $show['anon'] ) && isset ( $show['!anon'] ) )
+ || ( isset ( $show['patrolled'] ) && isset ( $show['!patrolled'] ) ) ) {
- $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
+ $this->dieUsageMsg( array( 'show' ) );
}
- // Check permissions
- global $wgUser;
- if((isset($show['patrolled']) || isset($show['!patrolled'])) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol())
- $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied');
+ // Check permissions.
+ if ( ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) && !$wgUser->useRCPatrol() && !$wgUser->useNPPatrol() )
+ $this->dieUsage( "You need the patrol right to request the patrolled flag", 'permissiondenied' );
/* Add additional conditions to query depending upon parameters. */
- $this->addWhereIf('rc_minor = 0', isset ($show['!minor']));
- $this->addWhereIf('rc_minor != 0', isset ($show['minor']));
- $this->addWhereIf('rc_bot = 0', isset ($show['!bot']));
- $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( 'rc_minor = 0', isset ( $show['!minor'] ) );
+ $this->addWhereIf( 'rc_minor != 0', isset ( $show['minor'] ) );
+ $this->addWhereIf( 'rc_bot = 0', isset ( $show['!bot'] ) );
+ $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'] ) );
}
+ if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) )
+ $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' );
+ if ( !is_null( $params['user'] ) )
+ $this->addWhereFld( 'rc_user_text', $params['user'] );
+ if ( !is_null( $params['excludeuser'] ) )
+ $this->addWhere( 'rc_user_text != ' . $this->getDB()->addQuotes( $params['excludeuser'] ) );
- # This is an index optimization for mysql, as done in the Special:Watchlist page
- $this->addWhereIf("rc_timestamp > ''", !isset ($params['start']) && !isset ($params['end']) && $wgDBtype == 'mysql');
+ $db = $this->getDB();
+
+ // This is an index optimization for mysql, as done in the Special:Watchlist page
+ $this->addWhereIf( "rc_timestamp > ''", !isset ( $params['start'] ) && !isset ( $params['end'] ) && $db->getType() == 'mysql' );
- $this->addOption('LIMIT', $params['limit'] +1);
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
$ids = array ();
$count = 0;
- $res = $this->select(__METHOD__);
+ $res = $this->select( __METHOD__ );
- $db = $this->getDB();
- while ($row = $db->fetchObject($res)) {
- if (++ $count > $params['limit']) {
+ 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('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
+ $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
break;
}
- if (is_null($resultPageSet)) {
- $vals = $this->extractRowInfo($row);
- $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals);
- if(!$fit)
+ if ( is_null( $resultPageSet ) ) {
+ $vals = $this->extractRowInfo( $row );
+ $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('start',
- wfTimestamp(TS_ISO_8601, $row->rc_timestamp));
+ $this->setContinueEnumParameter( 'start',
+ wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) );
break;
}
} else {
- if ($params['allrev']) {
- $ids[] = intval($row->rc_this_oldid);
+ if ( $params['allrev'] ) {
+ $ids[] = intval( $row->rc_this_oldid );
} else {
- $ids[] = intval($row->rc_cur_id);
+ $ids[] = intval( $row->rc_cur_id );
}
}
}
- $db->freeResult($res);
+ $db->freeResult( $res );
- if (is_null($resultPageSet)) {
- $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item');
+ if ( is_null( $resultPageSet ) ) {
+ $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' );
}
- elseif ($params['allrev']) {
- $resultPageSet->populateFromRevisionIDs($ids);
+ elseif ( $params['allrev'] ) {
+ $resultPageSet->populateFromRevisionIDs( $ids );
} else {
- $resultPageSet->populateFromPageIDs($ids);
+ $resultPageSet->populateFromPageIDs( $ids );
}
}
- private function extractRowInfo($row) {
+ private function extractRowInfo( $row ) {
$vals = array ();
- if ($this->fld_ids) {
- $vals['pageid'] = intval($row->rc_cur_id);
- $vals['revid'] = intval($row->rc_this_oldid);
+ if ( $this->fld_ids ) {
+ $vals['pageid'] = intval( $row->rc_cur_id );
+ $vals['revid'] = intval( $row->rc_this_oldid );
}
- if ($this->fld_title)
- ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->rc_namespace, $row->rc_title));
+ $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+
+ if ( $this->fld_title )
+ ApiQueryBase::addTitleInfo( $vals, $title );
- if ($this->fld_user) {
+ if ( $this->fld_user ) {
$vals['user'] = $row->rc_user_text;
- if (!$row->rc_user)
+ if ( !$row->rc_user )
$vals['anon'] = '';
}
- if ($this->fld_flags) {
- if ($row->rc_new)
+ if ( $this->fld_flags ) {
+ if ( $row->rc_new )
$vals['new'] = '';
- if ($row->rc_minor)
+ if ( $row->rc_minor )
$vals['minor'] = '';
- if ($row->rc_bot)
+ if ( $row->rc_bot )
$vals['bot'] = '';
}
- if ($this->fld_patrol && isset($row->rc_patrolled))
+ if ( $this->fld_patrol && isset( $row->rc_patrolled ) )
$vals['patrolled'] = '';
- if ($this->fld_timestamp)
- $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp);
+ if ( $this->fld_timestamp )
+ $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
- if ($this->fld_sizes) {
- $vals['oldlen'] = intval($row->rc_old_len);
- $vals['newlen'] = intval($row->rc_new_len);
+ if ( $this->fld_sizes ) {
+ $vals['oldlen'] = intval( $row->rc_old_len );
+ $vals['newlen'] = intval( $row->rc_new_len );
}
+
+ if ( $this->fld_notificationtimestamp )
+ $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null ) ? '' : wfTimestamp( TS_ISO_8601, $row->wl_notificationtimestamp );
- if ($this->fld_comment && isset( $row->rc_comment ))
+ if ( $this->fld_comment && isset( $row->rc_comment ) )
$vals['comment'] = $row->rc_comment;
+
+ if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
+ global $wgUser;
+ $vals['parsedcomment'] = $wgUser->getSkin()->formatComment( $row->rc_comment, $title );
+ }
return $vals;
}
@@ -268,6 +291,12 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
ApiBase :: PARAM_ISMULTI => true,
ApiBase :: PARAM_TYPE => 'namespace'
),
+ 'user' => array(
+ ApiBase :: PARAM_TYPE => 'user',
+ ),
+ 'excludeuser' => array(
+ ApiBase :: PARAM_TYPE => 'user',
+ ),
'dir' => array (
ApiBase :: PARAM_DFLT => 'older',
ApiBase :: PARAM_TYPE => array (
@@ -291,9 +320,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'flags',
'user',
'comment',
+ 'parsedcomment',
'timestamp',
'patrol',
'sizes',
+ 'notificationtimestamp'
)
),
'show' => array (
@@ -308,6 +339,12 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'patrolled',
'!patrolled',
)
+ ),
+ 'owner' => array (
+ ApiBase :: PARAM_TYPE => 'user'
+ ),
+ 'token' => array (
+ ApiBase :: PARAM_TYPE => 'string'
)
);
}
@@ -318,19 +355,35 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'start' => 'The timestamp to start enumerating from.',
'end' => 'The timestamp to end enumerating.',
'namespace' => 'Filter changes to only the given namespace(s).',
+ 'user' => 'Only list changes by this user',
+ 'excludeuser' => 'Don\'t list changes by this user',
'dir' => 'In which direction to enumerate pages.',
'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.',
'For example, to see only minor edits done by logged-in users, set show=minor|!anon'
- )
+ ),
+ 'owner' => "The name of the user whose watchlist you'd like to access",
+ 'token' => "Give a security token (settable in preferences) to allow access to another user's watchlist"
);
}
public function getDescription() {
return "Get all recent changes to pages in the logged in user's watchlist";
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'bad_wlowner', 'info' => 'Specified user does not exist' ),
+ array( 'code' => 'bad_wltoken', 'info' => 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences' ),
+ array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ),
+ array( 'code' => 'patrol', 'info' => 'patrol property is not available' ),
+ array( 'show' ),
+ array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ),
+ array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -338,11 +391,12 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
'api.php?action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment',
'api.php?action=query&list=watchlist&wlallrev&wlprop=ids|title|timestamp|user|comment',
'api.php?action=query&generator=watchlist&prop=info',
- 'api.php?action=query&generator=watchlist&gwlallrev&prop=revisions&rvprop=timestamp|user'
+ 'api.php?action=query&generator=watchlist&gwlallrev&prop=revisions&rvprop=timestamp|user',
+ 'api.php?action=query&list=watchlist&wlowner=Bob_Smith&wltoken=d8d562e9725ea1512894cdab28e5ceebc7f20237'
);
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryWatchlist.php 69986 2010-07-27 03:57:39Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryWatchlist.php 69932 2010-07-26 08:03:21Z tstarling $';
}
}
diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php
index f3982bcb..42d4005b 100644
--- a/includes/api/ApiQueryWatchlistRaw.php
+++ b/includes/api/ApiQueryWatchlistRaw.php
@@ -23,9 +23,9 @@
* 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' );
}
/**
@@ -36,92 +36,95 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
- public function __construct($query, $moduleName) {
- parent :: __construct($query, $moduleName, 'wr');
+ public function __construct( $query, $moduleName ) {
+ parent :: __construct( $query, $moduleName, 'wr' );
}
public function execute() {
$this->run();
}
- public function executeGenerator($resultPageSet) {
- $this->run($resultPageSet);
+ public function executeGenerator( $resultPageSet ) {
+ $this->run( $resultPageSet );
}
- private function run($resultPageSet = null) {
+ private function run( $resultPageSet = null ) {
global $wgUser;
- $this->selectNamedDB('watchlist', DB_SLAVE, 'watchlist');
+ $this->selectNamedDB( 'watchlist', DB_SLAVE, 'watchlist' );
- if (!$wgUser->isLoggedIn())
- $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
+ if ( !$wgUser->isLoggedIn() )
+ $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
$params = $this->extractRequestParams();
- $prop = array_flip((array)$params['prop']);
- $show = array_flip((array)$params['show']);
- if(isset($show['changed']) && isset($show['!changed']))
- $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show');
-
- $this->addTables('watchlist');
- $this->addFields(array('wl_namespace', 'wl_title'));
- $this->addFieldsIf('wl_notificationtimestamp', isset($prop['changed']));
- $this->addWhereFld('wl_user', $wgUser->getId());
- $this->addWhereFld('wl_namespace', $params['namespace']);
- $this->addWhereIf('wl_notificationtimestamp IS NOT NULL', isset($show['changed']));
- $this->addWhereIf('wl_notificationtimestamp IS NULL', isset($show['!changed']));
- if(isset($params['continue']))
+ $prop = array_flip( (array)$params['prop'] );
+ $show = array_flip( (array)$params['show'] );
+ if ( isset( $show['changed'] ) && isset( $show['!changed'] ) )
+ $this->dieUsageMsg( array( 'show' ) );
+
+ $this->addTables( 'watchlist' );
+ $this->addFields( array( 'wl_namespace', 'wl_title' ) );
+ $this->addFieldsIf( 'wl_notificationtimestamp', isset( $prop['changed'] ) );
+ $this->addWhereFld( 'wl_user', $wgUser->getId() );
+ $this->addWhereFld( 'wl_namespace', $params['namespace'] );
+ $this->addWhereIf( 'wl_notificationtimestamp IS NOT NULL', isset( $show['changed'] ) );
+ $this->addWhereIf( 'wl_notificationtimestamp IS NULL', isset( $show['!changed'] ) );
+
+ if ( isset( $params['continue'] ) )
{
- $cont = explode('|', $params['continue']);
- if(count($cont) != 2)
- $this->dieUsage("Invalid continue param. You should pass the " .
- "original value returned by the previous query", "_badcontinue");
- $ns = intval($cont[0]);
- $title = $this->getDB()->strencode($this->titleToKey($cont[1]));
- $this->addWhere("wl_namespace > '$ns' OR ".
- "(wl_namespace = '$ns' AND ".
- "wl_title >= '$title')");
+ $cont = explode( '|', $params['continue'] );
+ if ( count( $cont ) != 2 )
+ $this->dieUsage( "Invalid continue param. You should pass the " .
+ "original value returned by the previous query", "_badcontinue" );
+ $ns = intval( $cont[0] );
+ $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) );
+ $this->addWhere( "wl_namespace > '$ns' OR " .
+ "(wl_namespace = '$ns' AND " .
+ "wl_title >= '$title')" );
}
+
// Don't ORDER BY wl_namespace if it's constant in the WHERE clause
- if(count($params['namespace']) == 1)
- $this->addOption('ORDER BY', 'wl_title');
+ if ( count( $params['namespace'] ) == 1 )
+ $this->addOption( 'ORDER BY', 'wl_title' );
else
- $this->addOption('ORDER BY', 'wl_namespace, wl_title');
- $this->addOption('LIMIT', $params['limit'] + 1);
- $res = $this->select(__METHOD__);
+ $this->addOption( 'ORDER BY', 'wl_namespace, wl_title' );
+ $this->addOption( 'LIMIT', $params['limit'] + 1 );
+ $res = $this->select( __METHOD__ );
$db = $this->getDB();
$titles = array();
$count = 0;
- while($row = $db->fetchObject($res))
+ while ( $row = $db->fetchObject( $res ) )
{
- if(++$count > $params['limit'])
+ if ( ++$count > $params['limit'] )
{
// We've reached the one extra which shows that there are additional pages to be had. Stop here...
- $this->setContinueEnumParameter('continue', $row->wl_namespace . '|' .
- $this->keyToTitle($row->wl_title));
+ $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' .
+ $this->keyToTitle( $row->wl_title ) );
break;
}
- $t = Title::makeTitle($row->wl_namespace, $row->wl_title);
- if(is_null($resultPageSet))
+ $t = Title::makeTitle( $row->wl_namespace, $row->wl_title );
+
+ if ( is_null( $resultPageSet ) )
{
$vals = array();
- ApiQueryBase::addTitleInfo($vals, $t);
- if(isset($prop['changed']) && !is_null($row->wl_notificationtimestamp))
- $vals['changed'] = wfTimestamp(TS_ISO_8601, $row->wl_notificationtimestamp);
- $fit = $this->getResult()->addValue($this->getModuleName(), null, $vals);
- if(!$fit)
+ ApiQueryBase::addTitleInfo( $vals, $t );
+ if ( isset( $prop['changed'] ) && !is_null( $row->wl_notificationtimestamp ) )
+ $vals['changed'] = wfTimestamp( TS_ISO_8601, $row->wl_notificationtimestamp );
+ $fit = $this->getResult()->addValue( $this->getModuleName(), null, $vals );
+ if ( !$fit )
{
- $this->setContinueEnumParameter('continue', $row->wl_namespace . '|' .
- $this->keyToTitle($row->wl_title));
+ $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' .
+ $this->keyToTitle( $row->wl_title ) );
break;
}
}
else
$titles[] = $t;
}
- if(is_null($resultPageSet))
- $this->getResult()->setIndexedTagName_internal($this->getModuleName(), 'wr');
+ if ( is_null( $resultPageSet ) )
+ $this->getResult()->setIndexedTagName_internal( $this->getModuleName(), 'wr' );
else
- $resultPageSet->populateFromTitles($titles);
+ $resultPageSet->populateFromTitles( $titles );
}
public function getAllowedParams() {
@@ -167,6 +170,13 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
public function getDescription() {
return "Get all pages on the logged in user's watchlist";
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ),
+ array( 'show' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -176,6 +186,6 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiQueryWatchlistRaw.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiQueryWatchlistRaw.php 69578 2010-07-20 02:46:20Z tstarling $';
}
} \ No newline at end of file
diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php
index 3dbee08a..64c2c3fb 100644
--- a/includes/api/ApiResult.php
+++ b/includes/api/ApiResult.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -53,8 +53,8 @@ class ApiResult extends ApiBase {
* Constructor
* @param $main ApiMain object
*/
- public function __construct($main) {
- parent :: __construct($main, 'result');
+ public function __construct( $main ) {
+ parent :: __construct( $main, 'result' );
$this->mIsRawMode = false;
$this->mCheckingSize = true;
$this->reset();
@@ -91,21 +91,21 @@ class ApiResult extends ApiBase {
public function getData() {
return $this->mData;
}
-
+
/**
* Get the 'real' size of a result item. This means the strlen() of the item,
* or the sum of the strlen()s of the elements if the item is an array.
* @param $value mixed
* @return int
*/
- public static function size($value) {
+ public static function size( $value ) {
$s = 0;
- if(is_array($value))
- foreach($value as $v)
- $s += self::size($v);
- else if(!is_object($value))
+ if ( is_array( $value ) )
+ foreach ( $value as $v )
+ $s += self::size( $v );
+ else if ( !is_object( $value ) )
// Objects can't always be cast to string
- $s = strlen($value);
+ $s = strlen( $value );
return $s;
}
@@ -116,7 +116,7 @@ class ApiResult extends ApiBase {
public function getSize() {
return $this->mSize;
}
-
+
/**
* Disable size checking in addValue(). Don't use this unless you
* REALLY know what you're doing. Values added while size checking
@@ -125,7 +125,7 @@ class ApiResult extends ApiBase {
public function disableSizeCheck() {
$this->mCheckingSize = false;
}
-
+
/**
* Re-enable size checking in addValue()
*/
@@ -140,21 +140,21 @@ class ApiResult extends ApiBase {
* @param $name string Index of $arr to add $value at
* @param $value mixed
*/
- public static function setElement(& $arr, $name, $value) {
- if ($arr === null || $name === null || $value === null || !is_array($arr) || is_array($name))
- ApiBase :: dieDebug(__METHOD__, 'Bad parameter');
+ public static function setElement( & $arr, $name, $value ) {
+ if ( $arr === null || $name === null || $value === null || !is_array( $arr ) || is_array( $name ) )
+ ApiBase :: dieDebug( __METHOD__, 'Bad parameter' );
- if (!isset ($arr[$name])) {
+ if ( !isset ( $arr[$name] ) ) {
$arr[$name] = $value;
}
- elseif (is_array($arr[$name]) && is_array($value)) {
- $merged = array_intersect_key($arr[$name], $value);
- if (!count($merged))
+ elseif ( is_array( $arr[$name] ) && is_array( $value ) ) {
+ $merged = array_intersect_key( $arr[$name], $value );
+ if ( !count( $merged ) )
$arr[$name] += $value;
else
- ApiBase :: dieDebug(__METHOD__, "Attempting to merge element $name");
+ ApiBase :: dieDebug( __METHOD__, "Attempting to merge element $name" );
} else
- ApiBase :: dieDebug(__METHOD__, "Attempting to add element $name=$value, existing value is {$arr[$name]}");
+ ApiBase :: dieDebug( __METHOD__, "Attempting to add element $name=$value, existing value is {$arr[$name]}" );
}
/**
@@ -165,15 +165,15 @@ class ApiResult extends ApiBase {
* as a sub item of $arr. Use this parameter to create elements in
* format <elem>text</elem> without attributes
*/
- public static function setContent(& $arr, $value, $subElemName = null) {
- if (is_array($value))
- ApiBase :: dieDebug(__METHOD__, 'Bad parameter');
- if (is_null($subElemName)) {
- ApiResult :: setElement($arr, '*', $value);
+ public static function setContent( & $arr, $value, $subElemName = null ) {
+ if ( is_array( $value ) )
+ ApiBase :: dieDebug( __METHOD__, 'Bad parameter' );
+ if ( is_null( $subElemName ) ) {
+ ApiResult :: setElement( $arr, '*', $value );
} else {
- if (!isset ($arr[$subElemName]))
+ if ( !isset ( $arr[$subElemName] ) )
$arr[$subElemName] = array ();
- ApiResult :: setElement($arr[$subElemName], '*', $value);
+ ApiResult :: setElement( $arr[$subElemName], '*', $value );
}
}
@@ -184,12 +184,12 @@ class ApiResult extends ApiBase {
* @param $arr array
* @param $tag string Tag name
*/
- public function setIndexedTagName(& $arr, $tag) {
+ public function setIndexedTagName( & $arr, $tag ) {
// In raw mode, add the '_element', otherwise just ignore
- if (!$this->getIsRawMode())
+ if ( !$this->getIsRawMode() )
return;
- if ($arr === null || $tag === null || !is_array($arr) || is_array($tag))
- ApiBase :: dieDebug(__METHOD__, 'Bad parameter');
+ if ( $arr === null || $tag === null || !is_array( $arr ) || is_array( $tag ) )
+ ApiBase :: dieDebug( __METHOD__, 'Bad parameter' );
// Do not use setElement() as it is ok to call this more than once
$arr['_element'] = $tag;
}
@@ -199,17 +199,16 @@ class ApiResult extends ApiBase {
* @param $arr array
* @param $tag string Tag name
*/
- public function setIndexedTagName_recursive(&$arr, $tag)
- {
- if(!is_array($arr))
- return;
- foreach($arr as &$a)
- {
- if(!is_array($a))
- continue;
- $this->setIndexedTagName($a, $tag);
- $this->setIndexedTagName_recursive($a, $tag);
- }
+ public function setIndexedTagName_recursive( &$arr, $tag ) {
+ if ( !is_array( $arr ) )
+ return;
+ foreach ( $arr as &$a )
+ {
+ if ( !is_array( $a ) )
+ continue;
+ $this->setIndexedTagName( $a, $tag );
+ $this->setIndexedTagName_recursive( $a, $tag );
+ }
}
/**
@@ -221,15 +220,15 @@ class ApiResult extends ApiBase {
*/
public function setIndexedTagName_internal( $path, $tag ) {
$data = & $this->mData;
- foreach((array)$path as $p) {
+ foreach ( (array)$path as $p ) {
if ( !isset( $data[$p] ) ) {
$data[$p] = array();
}
$data = & $data[$p];
}
- if(is_null($data))
+ if ( is_null( $data ) )
return;
- $this->setIndexedTagName($data, $tag);
+ $this->setIndexedTagName( $data, $tag );
}
/**
@@ -239,34 +238,34 @@ class ApiResult extends ApiBase {
* If $name is empty, the $value is added as a next list element data[] = $value
* @return bool True if $value fits in the result, false if not
*/
- public function addValue($path, $name, $value) {
+ public function addValue( $path, $name, $value ) {
global $wgAPIMaxResultSize;
$data = & $this->mData;
- if( $this->mCheckingSize ) {
- $newsize = $this->mSize + self::size($value);
- if($newsize > $wgAPIMaxResultSize)
+ if ( $this->mCheckingSize ) {
+ $newsize = $this->mSize + self::size( $value );
+ if ( $newsize > $wgAPIMaxResultSize )
return false;
$this->mSize = $newsize;
}
- if (!is_null($path)) {
- if (is_array($path)) {
- foreach ($path as $p) {
- if (!isset ($data[$p]))
+ if ( !is_null( $path ) ) {
+ if ( is_array( $path ) ) {
+ foreach ( $path as $p ) {
+ if ( !isset ( $data[$p] ) )
$data[$p] = array ();
$data = & $data[$p];
}
} else {
- if (!isset ($data[$path]))
+ if ( !isset ( $data[$path] ) )
$data[$path] = array ();
$data = & $data[$path];
}
}
- if (!$name)
+ if ( !$name )
$data[] = $value; // Add list element
else
- ApiResult :: setElement($data, $name, $value); // Add named element
+ ApiResult :: setElement( $data, $name, $value ); // Add named element
return true;
}
@@ -277,16 +276,16 @@ class ApiResult extends ApiBase {
* @param $path array
* @param $name string
*/
- public function unsetValue($path, $name) {
+ public function unsetValue( $path, $name ) {
$data = & $this->mData;
- if(!is_null($path))
- foreach((array)$path as $p) {
- if(!isset($data[$p]))
+ if ( !is_null( $path ) )
+ foreach ( (array)$path as $p ) {
+ if ( !isset( $data[$p] ) )
return;
$data = & $data[$p];
}
- $this->mSize -= self::size($data[$name]);
- unset($data[$name]);
+ $this->mSize -= self::size( $data[$name] );
+ unset( $data[$name] );
}
/**
@@ -294,52 +293,25 @@ class ApiResult extends ApiBase {
*/
public function cleanUpUTF8()
{
- array_walk_recursive($this->mData, array('ApiResult', 'cleanUp_helper'));
+ array_walk_recursive( $this->mData, array( 'ApiResult', 'cleanUp_helper' ) );
}
/**
* Callback function for cleanUpUTF8()
*/
- private static function cleanUp_helper(&$s)
+ private static function cleanUp_helper( &$s )
{
- if(!is_string($s))
+ if ( !is_string( $s ) )
return;
- $s = UtfNormal::cleanUp($s);
+ global $wgContLang;
+ $s = $wgContLang->normalize( $s );
}
public function execute() {
- ApiBase :: dieDebug(__METHOD__, 'execute() is not supported on Result object');
+ ApiBase :: dieDebug( __METHOD__, 'execute() is not supported on Result object' );
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiResult.php 47447 2009-02-18 12:41:28Z tstarling $';
- }
-}
-
-/* For compatibility with PHP versions < 5.1.0, define our own array_intersect_key function. */
-if (!function_exists('array_intersect_key')) {
- function array_intersect_key($isec, $keys) {
- $argc = func_num_args();
-
- if ($argc > 2) {
- for ($i = 1; $isec && $i < $argc; $i++) {
- $arr = func_get_arg($i);
-
- foreach (array_keys($isec) as $key) {
- if (!isset($arr[$key]))
- unset($isec[$key]);
- }
- }
-
- return $isec;
- } else {
- $res = array();
- foreach (array_keys($isec) as $key) {
- if (isset($keys[$key]))
- $res[$key] = $isec[$key];
- }
-
- return $res;
- }
+ return __CLASS__ . ': $Id: ApiResult.php 62354 2010-02-12 06:44:16Z mah $';
}
}
diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php
index 0f0eae10..5c259f4e 100644
--- a/includes/api/ApiRollback.php
+++ b/includes/api/ApiRollback.php
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -32,53 +32,51 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiRollback extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
$params = $this->extractRequestParams();
- $titleObj = NULL;
- if(!isset($params['title']))
- $this->dieUsageMsg(array('missingparam', 'title'));
- if(!isset($params['user']))
- $this->dieUsageMsg(array('missingparam', 'user'));
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
-
- $titleObj = Title::newFromText($params['title']);
- if(!$titleObj)
- $this->dieUsageMsg(array('invalidtitle', $params['title']));
- if(!$titleObj->exists())
- $this->dieUsageMsg(array('notanarticle'));
-
- #We need to be able to revert IPs, but getCanonicalName rejects them
- $username = User::isIP($params['user'])
+ $titleObj = null;
+ if ( !isset( $params['title'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'title' ) );
+ if ( !isset( $params['user'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'user' ) );
+
+ $titleObj = Title::newFromText( $params['title'] );
+ if ( !$titleObj )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
+ if ( !$titleObj->exists() )
+ $this->dieUsageMsg( array( 'notanarticle' ) );
+
+ // We need to be able to revert IPs, but getCanonicalName rejects them
+ $username = User::isIP( $params['user'] )
? $params['user']
- : User::getCanonicalName($params['user']);
- if(!$username)
- $this->dieUsageMsg(array('invaliduser', $params['user']));
+ : User::getCanonicalName( $params['user'] );
+ if ( !$username )
+ $this->dieUsageMsg( array( 'invaliduser', $params['user'] ) );
- $articleObj = new Article($titleObj);
- $summary = (isset($params['summary']) ? $params['summary'] : "");
+ $articleObj = new Article( $titleObj );
+ $summary = ( isset( $params['summary'] ) ? $params['summary'] : "" );
$details = null;
- $retval = $articleObj->doRollback($username, $summary, $params['token'], $params['markbot'], $details);
+ $retval = $articleObj->doRollback( $username, $summary, $params['token'], $params['markbot'], $details );
- if($retval)
+ if ( $retval )
// We don't care about multiple errors, just report one of them
- $this->dieUsageMsg(reset($retval));
+ $this->dieUsageMsg( reset( $retval ) );
$info = array(
'title' => $titleObj->getPrefixedText(),
- 'pageid' => intval($details['current']->getPage()),
+ 'pageid' => intval( $details['current']->getPage() ),
'summary' => $details['summary'],
- 'revid' => intval($titleObj->getLatestRevID()),
- 'old_revid' => intval($details['current']->getID()),
- 'last_revid' => intval($details['target']->getID())
+ 'revid' => intval( $details['newid'] ),
+ 'old_revid' => intval( $details['current']->getID() ),
+ 'last_revid' => intval( $details['target']->getID() )
);
- $this->getResult()->addValue(null, $this->getModuleName(), $info);
+ $this->getResult()->addValue( null, $this->getModuleName(), $info );
}
public function mustBePosted() { return true; }
@@ -113,6 +111,16 @@ class ApiRollback extends ApiBase {
'they will all be rolled back.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'title' ),
+ array( 'missingparam', 'user' ),
+ array( 'invalidtitle', 'title' ),
+ array( 'notanarticle' ),
+ array( 'invaliduser', 'user' ),
+ ) );
+ }
protected function getExamples() {
return array (
@@ -122,6 +130,6 @@ class ApiRollback extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiRollback.php 48122 2009-03-07 12:58:41Z catrope $';
+ return __CLASS__ . ': $Id: ApiRollback.php 65371 2010-04-21 10:41:25Z tstarling $';
}
}
diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php
index 9216317a..2ffae504 100644
--- a/includes/api/ApiUnblock.php
+++ b/includes/api/ApiUnblock.php
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -35,8 +35,8 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiUnblock extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
/**
@@ -46,38 +46,37 @@ class ApiUnblock extends ApiBase {
global $wgUser;
$params = $this->extractRequestParams();
- if($params['gettoken'])
+ if ( $params['gettoken'] )
{
$res['unblocktoken'] = $wgUser->editToken();
- $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
return;
}
- if(is_null($params['id']) && is_null($params['user']))
- $this->dieUsageMsg(array('unblock-notarget'));
- if(!is_null($params['id']) && !is_null($params['user']))
- $this->dieUsageMsg(array('unblock-idanduser'));
- if(is_null($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
- if(!$wgUser->isAllowed('block'))
- $this->dieUsageMsg(array('cantunblock'));
+ if ( is_null( $params['id'] ) && is_null( $params['user'] ) )
+ $this->dieUsageMsg( array( 'unblock-notarget' ) );
+ if ( !is_null( $params['id'] ) && !is_null( $params['user'] ) )
+ $this->dieUsageMsg( array( 'unblock-idanduser' ) );
+
+ if ( !$wgUser->isAllowed( 'block' ) )
+ $this->dieUsageMsg( array( 'cantunblock' ) );
$id = $params['id'];
$user = $params['user'];
- $reason = (is_null($params['reason']) ? '' : $params['reason']);
- $retval = IPUnblockForm::doUnblock($id, $user, $reason, $range);
- if($retval)
- $this->dieUsageMsg($retval);
+ $reason = ( is_null( $params['reason'] ) ? '' : $params['reason'] );
+ $retval = IPUnblockForm::doUnblock( $id, $user, $reason, $range );
+ if ( $retval )
+ $this->dieUsageMsg( $retval );
- $res['id'] = intval($id);
+ $res['id'] = intval( $id );
$res['user'] = $user;
$res['reason'] = $reason;
- $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
@@ -108,6 +107,18 @@ class ApiUnblock extends ApiBase {
'Unblock a user.'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'unblock-notarget' ),
+ array( 'unblock-idanduser' ),
+ array( 'cantunblock' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array (
@@ -117,6 +128,6 @@ class ApiUnblock extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiUnblock.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiUnblock.php 62599 2010-02-16 21:59:16Z reedy $';
}
}
diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php
index ddc9f7f8..9efba5f3 100644
--- a/includes/api/ApiUndelete.php
+++ b/includes/api/ApiUndelete.php
@@ -22,9 +22,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ("ApiBase.php");
+ require_once ( "ApiBase.php" );
}
/**
@@ -32,58 +32,57 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiUndelete extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
global $wgUser;
$params = $this->extractRequestParams();
- $titleObj = NULL;
- if(!isset($params['title']))
- $this->dieUsageMsg(array('missingparam', 'title'));
- if(!isset($params['token']))
- $this->dieUsageMsg(array('missingparam', 'token'));
+ $titleObj = null;
+ if ( !isset( $params['title'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'title' ) );
- if(!$wgUser->isAllowed('undelete'))
- $this->dieUsageMsg(array('permdenied-undelete'));
- if($wgUser->isBlocked())
- $this->dieUsageMsg(array('blockedtext'));
- if(!$wgUser->matchEditToken($params['token']))
- $this->dieUsageMsg(array('sessionfailure'));
+ if ( !$wgUser->isAllowed( 'undelete' ) )
+ $this->dieUsageMsg( array( 'permdenied-undelete' ) );
- $titleObj = Title::newFromText($params['title']);
- if(!$titleObj)
- $this->dieUsageMsg(array('invalidtitle', $params['title']));
+ if ( $wgUser->isBlocked() )
+ $this->dieUsageMsg( array( 'blockedtext' ) );
+
+ $titleObj = Title::newFromText( $params['title'] );
+ if ( !$titleObj )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
// Convert timestamps
- if(!isset($params['timestamps']))
+ if ( !isset( $params['timestamps'] ) )
$params['timestamps'] = array();
- if(!is_array($params['timestamps']))
- $params['timestamps'] = array($params['timestamps']);
- foreach($params['timestamps'] as $i => $ts)
- $params['timestamps'][$i] = wfTimestamp(TS_MW, $ts);
+ if ( !is_array( $params['timestamps'] ) )
+ $params['timestamps'] = array( $params['timestamps'] );
+ foreach ( $params['timestamps'] as $i => $ts )
+ $params['timestamps'][$i] = wfTimestamp( TS_MW, $ts );
- $pa = new PageArchive($titleObj);
- $dbw = wfGetDB(DB_MASTER);
+ $pa = new PageArchive( $titleObj );
+ $dbw = wfGetDB( DB_MASTER );
$dbw->begin();
- $retval = $pa->undelete((isset($params['timestamps']) ? $params['timestamps'] : array()), $params['reason']);
- if(!is_array($retval))
- $this->dieUsageMsg(array('cannotundelete'));
+ $retval = $pa->undelete( ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), $params['reason'] );
+ if ( !is_array( $retval ) )
+ $this->dieUsageMsg( array( 'cannotundelete' ) );
- if($retval[1])
- wfRunHooks( 'FileUndeleteComplete',
- array($titleObj, array(), $wgUser, $params['reason']) );
+ if ( $retval[1] )
+ wfRunHooks( 'FileUndeleteComplete',
+ array( $titleObj, array(), $wgUser, $params['reason'] ) );
$info['title'] = $titleObj->getPrefixedText();
- $info['revisions'] = intval($retval[0]);
- $info['fileversions'] = intval($retval[1]);
- $info['reason'] = intval($retval[2]);
- $this->getResult()->addValue(null, $this->getModuleName(), $info);
+ $info['revisions'] = intval( $retval[0] );
+ $info['fileversions'] = intval( $retval[1] );
+ $info['reason'] = intval( $retval[2] );
+ $this->getResult()->addValue( null, $this->getModuleName(), $info );
}
- public function mustBePosted() { return true; }
+ public function mustBePosted() {
+ return true;
+ }
public function isWriteMode() {
return true;
@@ -115,6 +114,20 @@ class ApiUndelete extends ApiBase {
'retrieved through list=deletedrevs'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'title' ),
+ array( 'permdenied-undelete' ),
+ array( 'blockedtext' ),
+ array( 'invalidtitle', 'title' ),
+ array( 'cannotundelete' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
protected function getExamples() {
return array (
@@ -124,6 +137,6 @@ class ApiUndelete extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiUndelete.php 48091 2009-03-06 13:49:44Z catrope $';
+ return __CLASS__ . ': $Id: ApiUndelete.php 62599 2010-02-16 21:59:16Z reedy $';
}
}
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php
new file mode 100644
index 00000000..6b91b223
--- /dev/null
+++ b/includes/api/ApiUpload.php
@@ -0,0 +1,325 @@
+<?php
+/*
+ * Created on Aug 21, 2008
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2008 - 2010 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 ApiUpload extends ApiBase {
+ protected $mUpload = null;
+ protected $mParams;
+
+ public function __construct( $main, $action ) {
+ parent::__construct( $main, $action );
+ }
+
+ public function execute() {
+ global $wgUser, $wgAllowCopyUploads;
+
+ // Check whether upload is enabled
+ if ( !UploadBase::isEnabled() )
+ $this->dieUsageMsg( array( 'uploaddisabled' ) );
+
+ $this->mParams = $this->extractRequestParams();
+ $request = $this->getMain()->getRequest();
+
+ // Add the uploaded file to the params array
+ $this->mParams['file'] = $request->getFileName( 'file' );
+
+ // One and only one of the following parameters is needed
+ $this->requireOnlyOneParameter( $this->mParams,
+ 'sessionkey', 'file', 'url' );
+
+ if ( $this->mParams['sessionkey'] ) {
+ /**
+ * Upload stashed in a previous request
+ */
+ // Check the session key
+ if ( !isset( $_SESSION['wsUploadData'][$this->mParams['sessionkey']] ) )
+ $this->dieUsageMsg( array( 'invalid-session-key' ) );
+
+ $this->mUpload = new UploadFromStash();
+ $this->mUpload->initialize( $this->mParams['filename'],
+ $this->mParams['sessionkey'],
+ $_SESSION['wsUploadData'][$this->mParams['sessionkey']] );
+ } elseif ( isset( $this->mParams['filename'] ) ) {
+ /**
+ * Upload from url, etc
+ * Parameter filename is required
+ */
+
+ if ( isset( $this->mParams['file'] ) ) {
+ $this->mUpload = new UploadFromFile();
+ $this->mUpload->initialize(
+ $this->mParams['filename'],
+ $request->getFileTempName( 'file' ),
+ $request->getFileSize( 'file' )
+ );
+ } elseif ( isset( $this->mParams['url'] ) ) {
+ // make sure upload by url is enabled:
+ if ( !$wgAllowCopyUploads )
+ $this->dieUsageMsg( array( 'uploaddisabled' ) );
+
+ // make sure the current user can upload
+ if ( ! $wgUser->isAllowed( 'upload_by_url' ) )
+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
+
+ $this->mUpload = new UploadFromUrl();
+ $this->mUpload->initialize( $this->mParams['filename'],
+ $this->mParams['url'] );
+
+ $status = $this->mUpload->fetchFile();
+ if ( !$status->isOK() ) {
+ $this->dieUsage( $status->getWikiText(), 'fetchfileerror' );
+ }
+ }
+ } else $this->dieUsageMsg( array( 'missingparam', 'filename' ) );
+
+ if ( !isset( $this->mUpload ) )
+ $this->dieUsage( 'No upload module set', 'nomodule' );
+
+ // Check whether the user has the appropriate permissions to upload anyway
+ $permission = $this->mUpload->isAllowed( $wgUser );
+
+ if ( $permission !== true ) {
+ if ( !$wgUser->isLoggedIn() )
+ $this->dieUsageMsg( array( 'mustbeloggedin', 'upload' ) );
+ else
+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
+ }
+ // Perform the upload
+ $result = $this->performUpload();
+
+ // Cleanup any temporary mess
+ $this->mUpload->cleanupTempFile();
+
+ $this->getResult()->addValue( null, $this->getModuleName(), $result );
+ }
+
+ protected function performUpload() {
+ global $wgUser;
+ $result = array();
+ $permErrors = $this->mUpload->verifyPermissions( $wgUser );
+ if ( $permErrors !== true ) {
+ $this->dieUsageMsg( array( 'badaccess-groups' ) );
+ }
+
+ // TODO: Move them to ApiBase's message map
+ $verification = $this->mUpload->verifyUpload();
+ if ( $verification['status'] !== UploadBase::OK ) {
+ $result['result'] = 'Failure';
+ switch( $verification['status'] ) {
+ case UploadBase::EMPTY_FILE:
+ $this->dieUsage( 'The file you submitted was empty', 'empty-file' );
+ break;
+ case UploadBase::FILETYPE_MISSING:
+ $this->dieUsage( 'The file is missing an extension', 'filetype-missing' );
+ break;
+ case UploadBase::FILETYPE_BADTYPE:
+ global $wgFileExtensions;
+ $this->dieUsage( 'This type of file is banned', 'filetype-banned',
+ 0, array(
+ 'filetype' => $verification['finalExt'],
+ 'allowed' => $wgFileExtensions
+ ) );
+ break;
+ case UploadBase::MIN_LENGTH_PARTNAME:
+ $this->dieUsage( 'The filename is too short', 'filename-tooshort' );
+ break;
+ case UploadBase::ILLEGAL_FILENAME:
+ $this->dieUsage( 'The filename is not allowed', 'illegal-filename',
+ 0, array( 'filename' => $verification['filtered'] ) );
+ break;
+ case UploadBase::OVERWRITE_EXISTING_FILE:
+ $this->dieUsage( 'Overwriting an existing file is not allowed', 'overwrite' );
+ break;
+ case UploadBase::VERIFICATION_ERROR:
+ $this->getResult()->setIndexedTagName( $verification['details'], 'detail' );
+ $this->dieUsage( 'This file did not pass file verification', 'verification-error',
+ 0, array( 'details' => $verification['details'] ) );
+ break;
+ case UploadBase::HOOK_ABORTED:
+ $this->dieUsage( "The modification you tried to make was aborted by an extension hook",
+ 'hookaborted', 0, array( 'error' => $verification['error'] ) );
+ break;
+ default:
+ $this->dieUsage( 'An unknown error occurred', 'unknown-error',
+ 0, array( 'code' => $verification['status'] ) );
+ break;
+ }
+ return $result;
+ }
+ if ( !$this->mParams['ignorewarnings'] ) {
+ $warnings = $this->mUpload->checkWarnings();
+ if ( $warnings ) {
+ // Add indices
+ $this->getResult()->setIndexedTagName( $warnings, 'warning' );
+
+ if ( isset( $warnings['duplicate'] ) ) {
+ $dupes = array();
+ foreach ( $warnings['duplicate'] as $key => $dupe )
+ $dupes[] = $dupe->getName();
+ $this->getResult()->setIndexedTagName( $dupes, 'duplicate' );
+ $warnings['duplicate'] = $dupes;
+ }
+
+
+ if ( isset( $warnings['exists'] ) ) {
+ $warning = $warnings['exists'];
+ unset( $warnings['exists'] );
+ $warnings[$warning['warning']] = $warning['file']->getName();
+ }
+
+ $result['result'] = 'Warning';
+ $result['warnings'] = $warnings;
+
+ $sessionKey = $this->mUpload->stashSession();
+ if ( !$sessionKey )
+ $this->dieUsage( 'Stashing temporary file failed', 'stashfailed' );
+
+ $result['sessionkey'] = $sessionKey;
+
+ return $result;
+ }
+ }
+
+ // Use comment as initial page text by default
+ if ( is_null( $this->mParams['text'] ) )
+ $this->mParams['text'] = $this->mParams['comment'];
+
+ // No errors, no warnings: do the upload
+ $status = $this->mUpload->performUpload( $this->mParams['comment'],
+ $this->mParams['text'], $this->mParams['watch'], $wgUser );
+
+ if ( !$status->isGood() ) {
+ $error = $status->getErrorsArray();
+ $this->getResult()->setIndexedTagName( $result['details'], 'error' );
+
+ $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error );
+ }
+
+ $file = $this->mUpload->getLocalFile();
+ $result['result'] = 'Success';
+ $result['filename'] = $file->getName();
+ $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
+
+ return $result;
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ $params = array(
+ 'filename' => null,
+ 'comment' => array(
+ ApiBase::PARAM_DFLT => ''
+ ),
+ 'text' => null,
+ 'token' => null,
+ 'watch' => false,
+ 'ignorewarnings' => false,
+ 'file' => null,
+ 'url' => null,
+ 'sessionkey' => null,
+ );
+ return $params;
+
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'filename' => 'Target filename',
+ 'token' => 'Edit token. You can get one of these through prop=info',
+ 'comment' => 'Upload comment. Also used as the initial page text for new files if "text" is not specified',
+ 'text' => 'Initial page text for new files',
+ 'watch' => 'Watch the page',
+ 'ignorewarnings' => 'Ignore any warnings',
+ 'file' => 'File contents',
+ 'url' => 'Url to fetch the file from',
+ 'sessionkey' => array(
+ 'Session key returned by a previous upload that failed due to warnings',
+ ),
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Upload a file, or get the status of pending uploads. Several methods are available:',
+ ' * Upload file contents directly, using the "file" parameter',
+ ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter',
+ ' * Complete an earlier upload that failed due to warnings, using the "sessionkey" parameter',
+ 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when',
+ 'sending the "file". Note also that queries using session keys must be',
+ 'done in the same login session as the query that originally returned the key (i.e. do not',
+ 'log out and then log back in). Also you must get and send an edit token before doing any upload stuff.'
+ );
+ }
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'uploaddisabled' ),
+ array( 'invalid-session-key' ),
+ array( 'uploaddisabled' ),
+ array( 'badaccess-groups' ),
+ array( 'missingparam', 'filename' ),
+ array( 'mustbeloggedin', 'upload' ),
+ array( 'badaccess-groups' ),
+ array( 'badaccess-groups' ),
+ array( 'code' => 'fetchfileerror', 'info' => '' ),
+ array( 'code' => 'nomodule', 'info' => 'No upload module set' ),
+ array( 'code' => 'empty-file', 'info' => 'The file you submitted was empty' ),
+ array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ),
+ array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ),
+ array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ),
+ array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ),
+ array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ return '';
+ }
+
+ protected function getExamples() {
+ return array(
+ 'Upload from a URL:',
+ ' api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png',
+ 'Complete an upload that failed due to warnings:',
+ ' api.php?action=upload&filename=Wiki.png&sessionkey=sessionkey&ignorewarnings=1',
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiUpload.php 51812 2009-06-12 23:45:20Z dale $';
+ }
+}
diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php
new file mode 100644
index 00000000..6296a8f8
--- /dev/null
+++ b/includes/api/ApiUserrights.php
@@ -0,0 +1,128 @@
+<?php
+
+/*
+ * Created on Mar 24, 2009
+ * API for MediaWiki 1.8+
+ *
+ * Copyright (C) 2009 Roan Kattouw <Firstname>.<Lastname>@home.nl
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ */
+
+if ( !defined( 'MEDIAWIKI' ) ) {
+ // Eclipse helper - will be ignored in production
+ require_once ( "ApiBase.php" );
+}
+
+/**
+ * @ingroup API
+ */
+class ApiUserrights extends ApiBase {
+
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
+ }
+
+ public function execute() {
+ $params = $this->extractRequestParams();
+
+ // User already validated in call to getTokenSalt from Main
+ $form = new UserrightsPage;
+ $user = $form->fetchUser( $params['user'] );
+
+ $r['user'] = $user->getName();
+ list( $r['added'], $r['removed'] ) =
+ $form->doSaveUserGroups(
+ $user, (array)$params['add'],
+ (array)$params['remove'], $params['reason'] );
+
+ $this->getResult()->setIndexedTagName( $r['added'], 'group' );
+ $this->getResult()->setIndexedTagName( $r['removed'], 'group' );
+ $this->getResult()->addValue( null, $this->getModuleName(), $r );
+ }
+
+ public function mustBePosted() {
+ return true;
+ }
+
+ public function isWriteMode() {
+ return true;
+ }
+
+ public function getAllowedParams() {
+ return array (
+ 'user' => null,
+ 'add' => array(
+ ApiBase :: PARAM_TYPE => User::getAllGroups(),
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'remove' => array(
+ ApiBase :: PARAM_TYPE => User::getAllGroups(),
+ ApiBase :: PARAM_ISMULTI => true
+ ),
+ 'token' => null,
+ 'reason' => array(
+ ApiBase :: PARAM_DFLT => ''
+ )
+ );
+ }
+
+ public function getParamDescription() {
+ return array (
+ 'user' => 'User name',
+ 'add' => 'Add the user to these groups',
+ 'remove' => 'Remove the user from these groups',
+ 'token' => 'A userrights token previously retrieved through list=users',
+ 'reason' => 'Reason for the change',
+ );
+ }
+
+ public function getDescription() {
+ return array(
+ 'Add/remove a user to/from groups',
+ );
+ }
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'missingparam', 'user' ),
+ ) );
+ }
+
+ public function getTokenSalt() {
+ $params = $this->extractRequestParams();
+ if ( is_null( $params['user'] ) )
+ $this->dieUsageMsg( array( 'missingparam', 'user' ) );
+
+ $form = new UserrightsPage;
+ $user = $form->fetchUser( $params['user'] );
+ if ( $user instanceof WikiErrorMsg )
+ $this->dieUsageMsg( array_merge(
+ (array)$user->getMessageKey(), $user->getMessageArgs() ) );
+
+ return $user->getName();
+ }
+
+ protected function getExamples() {
+ return array (
+ 'api.php?action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC'
+ );
+ }
+
+ public function getVersion() {
+ return __CLASS__ . ': $Id: ApiUserrights.php 62686 2010-02-19 01:25:57Z reedy $';
+ }
+}
diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php
index 7901b6ac..391d91e2 100644
--- a/includes/api/ApiWatch.php
+++ b/includes/api/ApiWatch.php
@@ -23,9 +23,9 @@
* http://www.gnu.org/copyleft/gpl.html
*/
-if (!defined('MEDIAWIKI')) {
+if ( !defined( 'MEDIAWIKI' ) ) {
// Eclipse helper - will be ignored in production
- require_once ('ApiBase.php');
+ require_once ( 'ApiBase.php' );
}
/**
@@ -35,21 +35,25 @@ if (!defined('MEDIAWIKI')) {
*/
class ApiWatch extends ApiBase {
- public function __construct($main, $action) {
- parent :: __construct($main, $action);
+ public function __construct( $main, $action ) {
+ parent :: __construct( $main, $action );
}
public function execute() {
global $wgUser;
- if(!$wgUser->isLoggedIn())
- $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin');
+ if ( !$wgUser->isLoggedIn() )
+ $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' );
+
$params = $this->extractRequestParams();
- $title = Title::newFromText($params['title']);
- if(!$title)
- $this->dieUsageMsg(array('invalidtitle', $params['title']));
- $article = new Article($title);
- $res = array('title' => $title->getPrefixedText());
- if($params['unwatch'])
+ $title = Title::newFromText( $params['title'] );
+
+ if ( !$title )
+ $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
+
+ $article = new Article( $title );
+ $res = array( 'title' => $title->getPrefixedText() );
+
+ if ( $params['unwatch'] )
{
$res['unwatched'] = '';
$success = $article->doUnwatch();
@@ -59,14 +63,14 @@ class ApiWatch extends ApiBase {
$res['watched'] = '';
$success = $article->doWatch();
}
- if(!$success)
- $this->dieUsageMsg(array('hookaborted'));
- $this->getResult()->addValue(null, $this->getModuleName(), $res);
+ if ( !$success )
+ $this->dieUsageMsg( array( 'hookaborted' ) );
+ $this->getResult()->addValue( null, $this->getModuleName(), $res );
}
public function isWriteMode() {
return true;
- }
+ }
public function getAllowedParams() {
return array (
@@ -87,6 +91,14 @@ class ApiWatch extends ApiBase {
'Add or remove a page from/to the current user\'s watchlist'
);
}
+
+ public function getPossibleErrors() {
+ return array_merge( parent::getPossibleErrors(), array(
+ array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ),
+ array( 'invalidtitle', 'title' ),
+ array( 'hookaborted' ),
+ ) );
+ }
protected function getExamples() {
return array(
@@ -96,6 +108,6 @@ class ApiWatch extends ApiBase {
}
public function getVersion() {
- return __CLASS__ . ': $Id: ApiWatch.php 69579 2010-07-20 02:49:55Z tstarling $';
+ return __CLASS__ . ': $Id: ApiWatch.php 69578 2010-07-20 02:46:20Z tstarling $';
}
}
diff --git a/includes/cbt/CBTCompiler.php b/includes/cbt/CBTCompiler.php
deleted file mode 100644
index 75955797..00000000
--- a/includes/cbt/CBTCompiler.php
+++ /dev/null
@@ -1,366 +0,0 @@
-<?php
-
-/**
- * This file contains functions to convert callback templates to other languages.
- * The template should first be pre-processed with CBTProcessor to remove static
- * sections.
- */
-
-
-require_once( dirname( __FILE__ ) . '/CBTProcessor.php' );
-
-/**
- * Push a value onto the stack
- * Argument 1: value
- */
-define( 'CBT_PUSH', 1 );
-
-/**
- * Pop, concatenate argument, push
- * Argument 1: value
- */
-define( 'CBT_CAT', 2 );
-
-/**
- * Concatenate where the argument is on the stack, instead of immediate
- */
-define( 'CBT_CATS', 3 );
-
-/**
- * Call a function, push the return value onto the stack and put it in the cache
- * Argument 1: argument count
- *
- * The arguments to the function are on the stack
- */
-define( 'CBT_CALL', 4 );
-
-/**
- * Pop, htmlspecialchars, push
- */
-define( 'CBT_HX', 5 );
-
-class CBTOp {
- var $opcode;
- var $arg1;
- var $arg2;
-
- function CBTOp( $opcode, $arg1, $arg2 ) {
- $this->opcode = $opcode;
- $this->arg1 = $arg1;
- $this->arg2 = $arg2;
- }
-
- function name() {
- $opcodeNames = array(
- CBT_PUSH => 'PUSH',
- CBT_CAT => 'CAT',
- CBT_CATS => 'CATS',
- CBT_CALL => 'CALL',
- CBT_HX => 'HX',
- );
- return $opcodeNames[$this->opcode];
- }
-};
-
-class CBTCompiler {
- var $mOps = array();
- var $mCode;
-
- function CBTCompiler( $text ) {
- $this->mText = $text;
- }
-
- /**
- * Compile the text.
- * Returns true on success, error message on failure
- */
- function compile() {
- $this->mLastError = false;
- $this->mOps = array();
-
- $this->doText( 0, strlen( $this->mText ) );
-
- if ( $this->mLastError !== false ) {
- $pos = $this->mErrorPos;
-
- // Find the line number at which the error occurred
- $startLine = 0;
- $endLine = 0;
- $line = 0;
- do {
- if ( $endLine ) {
- $startLine = $endLine + 1;
- }
- $endLine = strpos( $this->mText, "\n", $startLine );
- ++$line;
- } while ( $endLine !== false && $endLine < $pos );
-
- $text = "Template error at line $line: $this->mLastError\n<pre>\n";
-
- $context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
- $text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n</pre>\n";
- } else {
- $text = true;
- }
-
- return $text;
- }
-
- /** Shortcut for doOpenText( $start, $end, false */
- function doText( $start, $end ) {
- return $this->doOpenText( $start, $end, false );
- }
-
- function phpQuote( $text ) {
- return "'" . strtr( $text, array( "\\" => "\\\\", "'" => "\\'" ) ) . "'";
- }
-
- function op( $opcode, $arg1 = null, $arg2 = null) {
- return new CBTOp( $opcode, $arg1, $arg2 );
- }
-
- /**
- * 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
- * closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
- *
- * The parameter $p is advanced to the position after the closing brace,
- * or after the end. A CBTValue is returned.
- *
- * @private
- */
- function doOpenText( &$p, $end, $needClosing = true ) {
- $in =& $this->mText;
- $start = $p;
- $atStart = true;
-
- $foundClosing = false;
- while ( $p < $end ) {
- $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
- $pToken = $p + $matchLength;
-
- if ( $pToken >= $end ) {
- // No more braces, output remainder
- if ( $atStart ) {
- $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p ) );
- $atStart = false;
- } else {
- $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p ) );
- }
- $p = $end;
- break;
- }
-
- // Output the text before the brace
- if ( $atStart ) {
- $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $matchLength ) );
- $atStart = false;
- } else {
- $this->mOps[] = $this->op( CBT_CAT, substr( $in, $p, $matchLength ) );
- }
-
- // 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] == '"' ) {
- $this->doOpenFunction( $p, $end );
- if ( $p < $end && $in[$p] == '"' ) {
- $this->mOps[] = $this->op( CBT_HX );
- }
- } else {
- $this->doOpenFunction( $p, $end );
- }
- if ( $atStart ) {
- $atStart = false;
- } else {
- $this->mOps[] = $this->op( CBT_CATS );
- }
- }
- if ( $foundClosing && !$needClosing ) {
- $this->error( 'Errant closing brace', $p );
- } elseif ( !$foundClosing && $needClosing ) {
- $this->error( 'Unclosed text section', $start );
- } else {
- if ( $atStart ) {
- $this->mOps[] = $this->op( CBT_PUSH, '' );
- }
- }
- }
-
- /**
- * 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
- * closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
- *
- * The parameter $p is advanced to the position after the closing brace,
- * or after the end. A CBTValue is returned.
- *
- * @private
- */
- function doOpenFunction( &$p, $end, $needClosing = true ) {
- $in =& $this->mText;
- $start = $p;
- $argCount = 0;
-
- $foundClosing = false;
- while ( $p < $end ) {
- $char = $in[$p];
- if ( $char == '{' ) {
- // Switch to text mode
- ++$p;
- $this->doOpenText( $p, $end );
- ++$argCount;
- } elseif ( $char == '}' ) {
- // Block end
- ++$p;
- $foundClosing = true;
- break;
- } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
- // Whitespace
- // Consume the rest of the whitespace
- $p += strspn( $in, CBT_WHITE, $p, $end - $p );
- } else {
- // Token, find the end of it
- $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
- $this->mOps[] = $this->op( CBT_PUSH, substr( $in, $p, $tokenLength ) );
-
- // Execute the token as a function if it's not the function name
- if ( $argCount ) {
- $this->mOps[] = $this->op( CBT_CALL, 1 );
- }
-
- $p += $tokenLength;
- ++$argCount;
- }
- }
- if ( !$foundClosing && $needClosing ) {
- $this->error( 'Unclosed function', $start );
- return '';
- }
-
- $this->mOps[] = $this->op( CBT_CALL, $argCount );
- }
-
- /**
- * Set a flag indicating that an error has been found.
- */
- function error( $text, $pos = false ) {
- $this->mLastError = $text;
- if ( $pos === false ) {
- $this->mErrorPos = $this->mCurrentPos;
- } else {
- $this->mErrorPos = $pos;
- }
- }
-
- function getLastError() {
- return $this->mLastError;
- }
-
- function opsToString() {
- $s = '';
- foreach( $this->mOps as $op ) {
- $s .= $op->name();
- if ( !is_null( $op->arg1 ) ) {
- $s .= ' ' . var_export( $op->arg1, true );
- }
- if ( !is_null( $op->arg2 ) ) {
- $s .= ' ' . var_export( $op->arg2, true );
- }
- $s .= "\n";
- }
- return $s;
- }
-
- function generatePHP( $functionObj ) {
- $fname = 'CBTCompiler::generatePHP';
- wfProfileIn( $fname );
- $stack = array();
-
- foreach( $this->mOps as $op ) {
- switch( $op->opcode ) {
- case CBT_PUSH:
- $stack[] = $this->phpQuote( $op->arg1 );
- break;
- case CBT_CAT:
- $val = array_pop( $stack );
- array_push( $stack, "$val . " . $this->phpQuote( $op->arg1 ) );
- break;
- case CBT_CATS:
- $right = array_pop( $stack );
- $left = array_pop( $stack );
- array_push( $stack, "$left . $right" );
- break;
- case CBT_CALL:
- $args = array_slice( $stack, count( $stack ) - $op->arg1, $op->arg1 );
- $stack = array_slice( $stack, 0, count( $stack ) - $op->arg1 );
-
- // Some special optimised expansions
- if ( $op->arg1 == 0 ) {
- $result = '';
- } else {
- $func = array_shift( $args );
- if ( substr( $func, 0, 1 ) == "'" && substr( $func, -1 ) == "'" ) {
- $func = substr( $func, 1, strlen( $func ) - 2 );
- if ( $func == "if" ) {
- if ( $op->arg1 < 3 ) {
- // This should have been caught during processing
- return "Not enough arguments to if";
- } elseif ( $op->arg1 == 3 ) {
- $result = "(({$args[0]} != '') ? ({$args[1]}) : '')";
- } else {
- $result = "(({$args[0]} != '') ? ({$args[1]}) : ({$args[2]}))";
- }
- } elseif ( $func == "true" ) {
- $result = "true";
- } elseif( $func == "lbrace" || $func == "{" ) {
- $result = "{";
- } elseif( $func == "rbrace" || $func == "}" ) {
- $result = "}";
- } elseif ( $func == "escape" || $func == "~" ) {
- $result = "htmlspecialchars({$args[0]})";
- } else {
- // Known function name
- $result = "{$functionObj}->{$func}(" . implode( ', ', $args ) . ')';
- }
- } else {
- // Unknown function name
- $result = "call_user_func(array($functionObj, $func), " . implode( ', ', $args ) . ' )';
- }
- }
- array_push( $stack, $result );
- break;
- case CBT_HX:
- $val = array_pop( $stack );
- array_push( $stack, "htmlspecialchars( $val )" );
- break;
- default:
- return "Unknown opcode {$op->opcode}\n";
- }
- }
- wfProfileOut( $fname );
- if ( count( $stack ) !== 1 ) {
- return "Error, stack count incorrect\n";
- }
- return '
- global $cbtExecutingGenerated;
- ++$cbtExecutingGenerated;
- $output = ' . $stack[0] . ';
- --$cbtExecutingGenerated;
- return $output;
- ';
- }
-}
diff --git a/includes/cbt/CBTProcessor.php b/includes/cbt/CBTProcessor.php
deleted file mode 100644
index 4fa1a93b..00000000
--- a/includes/cbt/CBTProcessor.php
+++ /dev/null
@@ -1,539 +0,0 @@
-<?php
-
-/**
- * PHP version of the callback template processor
- * 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.
- */
-
-define( 'CBT_WHITE', " \t\r\n" );
-define( 'CBT_BRACE', '{}' );
-define( 'CBT_DELIM', CBT_WHITE . CBT_BRACE );
-define( 'CBT_DEBUG', 0 );
-
-$GLOBALS['cbtExecutingGenerated'] = 0;
-
-/**
- * Attempting to be a MediaWiki-independent module
- */
-if ( !function_exists( 'wfProfileIn' ) ) {
- function wfProfileIn() {}
-}
-if ( !function_exists( 'wfProfileOut' ) ) {
- function wfProfileOut() {}
-}
-
-/**
- * Escape text for inclusion in template
- */
-function cbt_escape( $text ) {
- return strtr( $text, array( '{' => '{[}', '}' => '{]}' ) );
-}
-
-/**
- * Create a CBTValue
- */
-function cbt_value( $text = '', $deps = array(), $isTemplate = false ) {
- global $cbtExecutingGenerated;
- if ( $cbtExecutingGenerated ) {
- return $text;
- } else {
- return new CBTValue( $text, $deps, $isTemplate );
- }
-}
-
-/**
- * A dependency-tracking value class
- * 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;
-
- /**
- * Create a new value
- * @param $text String: , default ''.
- * @param $deps Array: what this value depends on
- * @param $isTemplate Bool: whether the result needs compilation/execution, default 'false'.
- */
- function CBTValue( $text = '', $deps = array(), $isTemplate = false ) {
- $this->mText = $text;
- if ( !is_array( $deps ) ) {
- $this->mDeps = array( $deps ) ;
- } else {
- $this->mDeps = $deps;
- }
- $this->mIsTemplate = $isTemplate;
- }
-
- /** Concatenate two values, merging their dependencies */
- function cat( $val ) {
- if ( is_object( $val ) ) {
- $this->addDeps( $val );
- $this->mText .= $val->mText;
- } else {
- $this->mText .= $val;
- }
- }
-
- /** Add the dependencies of another value to this one */
- function addDeps( $values ) {
- if ( !is_array( $values ) ) {
- $this->mDeps = array_merge( $this->mDeps, $values->mDeps );
- } else {
- foreach ( $values as $val ) {
- if ( !is_object( $val ) ) {
- var_dump( debug_backtrace() );
- exit;
- }
- $this->mDeps = array_merge( $this->mDeps, $val->mDeps );
- }
- }
- }
-
- /** Remove a list of dependencies */
- function removeDeps( $deps ) {
- $this->mDeps = array_diff( $this->mDeps, $deps );
- }
-
- function setText( $text ) {
- $this->mText = $text;
- }
-
- function getText() {
- return $this->mText;
- }
-
- function getDeps() {
- return $this->mDeps;
- }
-
- /** If the value is a template, execute it */
- function execute( &$processor ) {
- if ( $this->mIsTemplate ) {
- $myProcessor = new CBTProcessor( $this->mText, $processor->mFunctionObj, $processor->mIgnorableDeps );
- $myProcessor->mCompiling = $processor->mCompiling;
- $val = $myProcessor->doText( 0, strlen( $this->mText ) );
- if ( $myProcessor->getLastError() ) {
- $processor->error( $myProcessor->getLastError() );
- $this->mText = '';
- } else {
- $this->mText = $val->mText;
- $this->addDeps( $val );
- }
- if ( !$processor->mCompiling ) {
- $this->mIsTemplate = false;
- }
- }
- }
-
- /** If the value is plain text, escape it for inclusion in a template */
- function templateEscape() {
- if ( !$this->mIsTemplate ) {
- $this->mText = cbt_escape( $this->mText );
- }
- }
-
- /** Return true if the value has no dependencies */
- function isStatic() {
- return count( $this->mDeps ) == 0;
- }
-}
-
-/**
- * Template processor, for compilation and execution
- */
-class CBTProcessor {
- var $mText, # The text being processed
- $mFunctionObj, # The object containing callback functions
- $mCompiling = false, # True if compiling to a template, false if executing to text
- $mIgnorableDeps = array(), # Dependency names which should be treated as static
- $mFunctionCache = array(), # A cache of function results keyed by argument hash
- $mLastError = false, # Last error message or false for no error
- $mErrorPos = 0, # Last error position
-
- /** Built-in functions */
- $mBuiltins = array(
- 'if' => 'bi_if',
- 'true' => 'bi_true',
- '[' => 'bi_lbrace',
- 'lbrace' => 'bi_lbrace',
- ']' => 'bi_rbrace',
- 'rbrace' => 'bi_rbrace',
- 'escape' => 'bi_escape',
- '~' => 'bi_escape',
- );
-
- /**
- * Create a template processor for a given text, callback object and static dependency list
- */
- function CBTProcessor( $text, $functionObj, $ignorableDeps = array() ) {
- $this->mText = $text;
- $this->mFunctionObj = $functionObj;
- $this->mIgnorableDeps = $ignorableDeps;
- }
-
- /**
- * Execute the template.
- * If $compile is true, produces an optimised template where functions with static
- * dependencies have been replaced by their return values.
- */
- function execute( $compile = false ) {
- $fname = 'CBTProcessor::execute';
- wfProfileIn( $fname );
- $this->mCompiling = $compile;
- $this->mLastError = false;
- $val = $this->doText( 0, strlen( $this->mText ) );
- $text = $val->getText();
- if ( $this->mLastError !== false ) {
- $pos = $this->mErrorPos;
-
- // Find the line number at which the error occurred
- $startLine = 0;
- $endLine = 0;
- $line = 0;
- do {
- if ( $endLine ) {
- $startLine = $endLine + 1;
- }
- $endLine = strpos( $this->mText, "\n", $startLine );
- ++$line;
- } while ( $endLine !== false && $endLine < $pos );
-
- $text = "Template error at line $line: $this->mLastError\n<pre>\n";
-
- $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;
- }
-
- /** Shortcut for execute(true) */
- function compile() {
- $fname = 'CBTProcessor::compile';
- wfProfileIn( $fname );
- $s = $this->execute( true );
- wfProfileOut( $fname );
- return $s;
- }
-
- /** Shortcut for doOpenText( $start, $end, false */
- function doText( $start, $end ) {
- 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.
- */
- function templateEscape( $text ) {
- if ( $this->mCompiling ) {
- return cbt_escape( $text );
- } else {
- return $text;
- }
- }
-
- /**
- * 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
- * closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
- *
- * The parameter $p is advanced to the position after the closing brace,
- * or after the end. A CBTValue is returned.
- *
- * @private
- */
- function doOpenText( &$p, $end, $needClosing = true ) {
- $fname = 'CBTProcessor::doOpenText';
- wfProfileIn( $fname );
- $in =& $this->mText;
- $start = $p;
- $ret = new CBTValue( '', array(), $this->mCompiling );
-
- $foundClosing = false;
- while ( $p < $end ) {
- $matchLength = strcspn( $in, CBT_BRACE, $p, $end - $p );
- $pToken = $p + $matchLength;
-
- if ( $pToken >= $end ) {
- // No more braces, output remainder
- $ret->cat( substr( $in, $p ) );
- $p = $end;
- break;
- }
-
- // Output the text before the brace
- $ret->cat( substr( $in, $p, $matchLength ) );
-
- // 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 );
- $val = $this->doOpenFunction( $p, $end );
- wfProfileIn( $fname );
- if ( $p < $end && $in[$p] == '"' ) {
- $val->setText( htmlspecialchars( $val->getText() ) );
- }
- $ret->cat( $val );
- } else {
- // Process the function mode component
- wfProfileOut( $fname );
- $ret->cat( $this->doOpenFunction( $p, $end ) );
- wfProfileIn( $fname );
- }
- }
- if ( $foundClosing && !$needClosing ) {
- $this->error( 'Errant closing brace', $p );
- } elseif ( !$foundClosing && $needClosing ) {
- $this->error( 'Unclosed text section', $start );
- }
- 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
- * closing brace will flag an error, if $needClosing is true, the lack
- * of a closing brace will flag an error.
- *
- * The parameter $p is advanced to the position after the closing brace,
- * or after the end. A CBTValue is returned.
- *
- * @private
- */
- function doOpenFunction( &$p, $end, $needClosing = true ) {
- $in =& $this->mText;
- $start = $p;
- $tokens = array();
- $unexecutedTokens = array();
-
- $foundClosing = false;
- while ( $p < $end ) {
- $char = $in[$p];
- if ( $char == '{' ) {
- // Switch to text mode
- ++$p;
- $tokenStart = $p;
- $token = $this->doOpenText( $p, $end );
- $tokens[] = $token;
- $unexecutedTokens[] = '{' . substr( $in, $tokenStart, $p - $tokenStart - 1 ) . '}';
- } elseif ( $char == '}' ) {
- // Block end
- ++$p;
- $foundClosing = true;
- break;
- } elseif ( false !== strpos( CBT_WHITE, $char ) ) {
- // Whitespace
- // Consume the rest of the whitespace
- $p += strspn( $in, CBT_WHITE, $p, $end - $p );
- } else {
- // Token, find the end of it
- $tokenLength = strcspn( $in, CBT_DELIM, $p, $end - $p );
- $token = new CBTValue( substr( $in, $p, $tokenLength ) );
- // Execute the token as a function if it's not the function name
- if ( count( $tokens ) ) {
- $tokens[] = $this->doFunction( array( $token ), $p );
- } else {
- $tokens[] = $token;
- }
- $unexecutedTokens[] = $token->getText();
-
- $p += $tokenLength;
- }
- }
- if ( !$foundClosing && $needClosing ) {
- $this->error( 'Unclosed function', $start );
- return '';
- }
-
- $val = $this->doFunction( $tokens, $start );
- if ( $this->mCompiling && !$val->isStatic() ) {
- $compiled = '';
- $first = true;
- foreach( $tokens as $i => $token ) {
- if ( $first ) {
- $first = false;
- } else {
- $compiled .= ' ';
- }
- if ( $token->isStatic() ) {
- if ( $i !== 0 ) {
- $compiled .= '{' . $token->getText() . '}';
- } else {
- $compiled .= $token->getText();
- }
- } else {
- $compiled .= $unexecutedTokens[$i];
- }
- }
-
- // 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 );
- }
- return $val;
- }
-
- /**
- * Execute a function, caching and returning the result value.
- * $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.
- */
- function doFunction( $tokens, $p ) {
- if ( count( $tokens ) == 0 ) {
- return new CBTValue;
- }
- $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.
- $ret->addDeps( $tokens );
-
- $this->mCurrentPos = $p;
- $func = array_shift( $tokens );
- $func = $func->getText();
-
- // Extract the text component from all the tokens
- // And convert any templates to plain text
- $textArgs = array();
- foreach ( $tokens as $token ) {
- $token->execute( $this );
- $textArgs[] = $token->getText();
- }
-
- // Try the local cache
- $cacheKey = $func . "\n" . implode( "\n", $textArgs );
- if ( isset( $this->mFunctionCache[$cacheKey] ) ) {
- $val = $this->mFunctionCache[$cacheKey];
- } elseif ( isset( $this->mBuiltins[$func] ) ) {
- $func = $this->mBuiltins[$func];
- $val = call_user_func_array( array( &$this, $func ), $tokens );
- $this->mFunctionCache[$cacheKey] = $val;
- } elseif ( method_exists( $this->mFunctionObj, $func ) ) {
- $profName = get_class( $this->mFunctionObj ) . '::' . $func;
- wfProfileIn( "$fname-callback" );
- wfProfileIn( $profName );
- $val = call_user_func_array( array( &$this->mFunctionObj, $func ), $textArgs );
- wfProfileOut( $profName );
- wfProfileOut( "$fname-callback" );
- $this->mFunctionCache[$cacheKey] = $val;
- } else {
- $this->error( "Call of undefined function \"$func\"", $p );
- $val = new CBTValue;
- }
- if ( !is_object( $val ) ) {
- $val = new CBTValue((string)$val);
- }
-
- if ( CBT_DEBUG ) {
- $unexpanded = $val;
- }
-
- // 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 = "
- . var_export( $unexpanded, true )
- . "expanded return = "
- . var_export( $ret, true )
- );
- }
-
- wfProfileOut( $fname );
- return $ret;
- }
-
- /**
- * Set a flag indicating that an error has been found.
- */
- function error( $text, $pos = false ) {
- $this->mLastError = $text;
- if ( $pos === false ) {
- $this->mErrorPos = $this->mCurrentPos;
- } else {
- $this->mErrorPos = $pos;
- }
- }
-
- function getLastError() {
- return $this->mLastError;
- }
-
- /** 'if' built-in function */
- function bi_if( $condition, $trueBlock, $falseBlock = null ) {
- if ( is_null( $condition ) ) {
- $this->error( "Missing condition in if" );
- return '';
- }
-
- if ( $condition->getText() != '' ) {
- return new CBTValue( $trueBlock->getText(),
- array_merge( $condition->getDeps(), $trueBlock->getDeps() ),
- $trueBlock->mIsTemplate );
- } else {
- if ( !is_null( $falseBlock ) ) {
- return new CBTValue( $falseBlock->getText(),
- array_merge( $condition->getDeps(), $falseBlock->getDeps() ),
- $falseBlock->mIsTemplate );
- } else {
- return new CBTValue( '', $condition->getDeps() );
- }
- }
- }
-
- /** 'true' built-in function */
- function bi_true() {
- return "true";
- }
-
- /** left brace built-in */
- function bi_lbrace() {
- return '{';
- }
-
- /** right brace built-in */
- function bi_rbrace() {
- return '}';
- }
-
- /**
- * escape built-in.
- * 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
deleted file mode 100644
index 30581661..00000000
--- a/includes/cbt/README
+++ /dev/null
@@ -1,108 +0,0 @@
-Overview
---------
-
-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
-the dependencies -- the global data upon which the returned text depends. This
-allows a compiler to produce a template optimised for a certain context. For
-example, a user-dependent template can be produced, with the username replaced
-by static text, as well as all user preference dependent text.
-
-This was an experimental project to prove the concept -- to explore possible
-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.
-
-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.
-
-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
-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
- 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.
-
-* Cache invalidation. This is a simple implementation issue, although it would
- require extensive modification to the MediaWiki core.
-
-* Syntax. The syntax is minimalistic and easy to parse, but can be quite ugly.
- Will generations of MediaWiki users curse my name?
-
-* Security. The code produced by TemplateCompiler is best stored in memcached
- and executed with eval(). This allows anyone with access to the memcached port
- to run code as the apache user.
-
-
-Template syntax
----------------
-
-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.
-
-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
-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
-passed to the named function as parameters. The return value of the function
-is passed through to the output.
-
-Example:
- {escape {"hello"}}
-
-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.
-
-Example:
- {if title {<h1>{title}</h1>}}
-
-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
-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
-opening brace, and immediately follows the closing brace, with no whitespace.
-
-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
-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
-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
-shortcut, if there is only one dependency, $deps may be a string.
-
-
----------------------
-Tim Starling 2006
diff --git a/includes/db/Database.php b/includes/db/Database.php
index 52a59c11..ea5d77da 100644
--- a/includes/db/Database.php
+++ b/includes/db/Database.php
@@ -19,7 +19,7 @@ define( 'DEADLOCK_DELAY_MAX', 1500000 );
* Database abstraction object
* @ingroup Database
*/
-class Database {
+abstract class DatabaseBase {
#------------------------------------------------------------------------------
# Variables
@@ -39,6 +39,7 @@ class Database {
protected $mErrorCount = 0;
protected $mLBInfo = array();
protected $mFakeSlaveLag = null, $mFakeMaster = false;
+ protected $mDefaultBigSelects = null;
#------------------------------------------------------------------------------
# Accessors
@@ -49,7 +50,7 @@ class Database {
* Fail function, takes a Database as a parameter
* Set to false for default, 1 for ignore errors
*/
- function failFunction( $function = NULL ) {
+ function failFunction( $function = null ) {
return wfSetVar( $this->mFailFunction, $function );
}
@@ -64,7 +65,7 @@ class Database {
/**
* Boolean, controls output of large amounts of debug information
*/
- function debug( $debug = NULL ) {
+ function debug( $debug = null ) {
return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
}
@@ -72,7 +73,7 @@ class Database {
* 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 ) {
+ function bufferResults( $buffer = null ) {
if ( is_null( $buffer ) ) {
return !(bool)( $this->mFlags & DBO_NOBUFFER );
} else {
@@ -87,7 +88,7 @@ class Database {
* code should use lastErrno() and lastError() to handle the
* situation as appropriate.
*/
- function ignoreErrors( $ignoreErrors = NULL ) {
+ function ignoreErrors( $ignoreErrors = null ) {
return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
}
@@ -95,14 +96,14 @@ class Database {
* The current depth of nested transactions
* @param $level Integer: , default NULL.
*/
- function trxLevel( $level = NULL ) {
+ function trxLevel( $level = null ) {
return wfSetVar( $this->mTrxLevel, $level );
}
/**
* Number of errors logged, only useful when errors are ignored
*/
- function errorCount( $count = NULL ) {
+ function errorCount( $count = null ) {
return wfSetVar( $this->mErrorCount, $count );
}
@@ -113,19 +114,19 @@ class Database {
/**
* Properties passed down from the server info array of the load balancer
*/
- function getLBInfo( $name = NULL ) {
+ 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;
+ return null;
}
}
}
- function setLBInfo( $name, $value = NULL ) {
+ function setLBInfo( $name, $value = null ) {
if ( is_null( $value ) ) {
$this->mLBInfo = $name;
} else {
@@ -192,6 +193,14 @@ class Database {
}
/**
+ * Returns true if this database requires that SELECT DISTINCT queries require that all
+ ORDER BY expressions occur in the SELECT list per the SQL92 standard
+ */
+ function standardSelectDistinct() {
+ 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';
*/
@@ -225,14 +234,37 @@ class Database {
*/
function isOpen() { return $this->mOpened; }
+ /**
+ * Set a flag for this connection
+ *
+ * @param $flag Integer: DBO_* constants from Defines.php:
+ * - DBO_DEBUG: output some debug info (same as debug())
+ * - DBO_NOBUFFER: don't buffer results (inverse of bufferResults())
+ * - DBO_IGNORE: ignore errors (same as ignoreErrors())
+ * - DBO_TRX: automatically start transactions
+ * - DBO_DEFAULT: automatically sets DBO_TRX if not in command line mode
+ * and removes it in command line mode
+ * - DBO_PERSISTENT: use persistant database connection
+ */
function setFlag( $flag ) {
$this->mFlags |= $flag;
}
+ /**
+ * Clear a flag for this connection
+ *
+ * @param $flag: same as setFlag()'s $flag param
+ */
function clearFlag( $flag ) {
$this->mFlags &= ~$flag;
}
+ /**
+ * Returns a boolean whether the flag $flag is set for this connection
+ *
+ * @param $flag: same as setFlag()'s $flag param
+ * @return Boolean
+ */
function getFlag( $flag ) {
return !!($this->mFlags & $flag);
}
@@ -252,6 +284,11 @@ class Database {
}
}
+ /**
+ * Get the type of the DBMS, as it appears in $wgDBtype.
+ */
+ abstract function getType();
+
#------------------------------------------------------------------------------
# Other functions
#------------------------------------------------------------------------------
@@ -272,7 +309,7 @@ class Database {
global $wgOut, $wgDBprefix, $wgCommandLineMode;
# Can't get a reference if it hasn't been set yet
if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
+ $wgOut = null;
}
$this->mFailFunction = $failFunction;
@@ -306,7 +343,7 @@ class Database {
}
/**
- * Same as new Database( ... ), kept for backward compatibility
+ * Same as new DatabaseMysql( ... ), kept for backward compatibility
* @param $server String: database server host
* @param $user String: database user name
* @param $password String: database user password
@@ -316,7 +353,7 @@ class Database {
*/
static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
{
- return new Database( $server, $user, $password, $dbName, $failFunction, $flags );
+ return new DatabaseMysql( $server, $user, $password, $dbName, $failFunction, $flags );
}
/**
@@ -327,114 +364,7 @@ class Database {
* @param $password String: database user password
* @param $dbName String: database name
*/
- function open( $server, $user, $password, $dbName ) {
- global $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");
-
- # The kernel's default SYN retransmission period is far too slow for us,
- # so we use a short timeout plus a manual retry. Retrying means that a small
- # but finite rate of SYN packet loss won't cause user-visible errors.
- $this->mConn = false;
- if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
- $numAttempts = 2;
- } else {
- $numAttempts = 1;
- }
- $this->installErrorHandler();
- for ( $i = 0; $i < $numAttempts && !$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();
- # Always log connection errors
- if ( !$this->mConn ) {
- $error = $this->lastError();
- if ( !$error ) {
- $error = $phpError;
- }
- wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
- wfDebug( "DB connection error\n" );
- wfDebug( "Server: $server, User: $user, Password: " .
- substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
- $success = false;
- }
-
- wfProfileOut("dbconnect-$server");
-
- if ( $dbName != '' && $this->mConn !== false ) {
- $success = @/**/mysql_select_db( $dbName, $this->mConn );
- if ( !$success ) {
- $error = "Error selecting database $dbName on server {$this->mServer} " .
- "from client host " . wfHostname() . "\n";
- wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
- wfDebug( $error );
- }
- } else {
- # Delay USE query
- $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;
- }
+ abstract function open( $server, $user, $password, $dbName );
protected function installErrorHandler() {
$this->mPHPError = false;
@@ -466,17 +396,9 @@ class Database {
*
* @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;
- }
+ function close() {
+ # Stub, should probably be overridden
+ return true;
}
/**
@@ -505,7 +427,7 @@ class Database {
* Should return true if unsure.
*/
function isWriteQuery( $sql ) {
- return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW)\b/i', $sql );
+ return !preg_match( '/^(?:SELECT|BEGIN|COMMIT|SET|SHOW|\(SELECT)\b/i', $sql );
}
/**
@@ -629,14 +551,7 @@ class Database {
* @return Result object to feed to fetchObject, fetchRow, ...; or false on failure
* @private
*/
- /*private*/ function doQuery( $sql ) {
- if( $this->bufferResults() ) {
- $ret = mysql_query( $sql, $this->mConn );
- } else {
- $ret = mysql_unbuffered_query( $sql, $this->mConn );
- }
- return $ret;
- }
+ /*private*/ abstract function doQuery( $sql );
/**
* @param $error String
@@ -762,12 +677,8 @@ class Database {
* @param $res Mixed: A SQL result
*/
function freeResult( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- if ( !@/**/mysql_free_result( $res ) ) {
- throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
- }
+ # Stub. Might not really need to be overridden, since results should
+ # be freed by PHP when the variable goes out of scope anyway.
}
/**
@@ -779,16 +690,7 @@ class Database {
* @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;
- }
+ abstract function fetchObject( $res );
/**
* Fetch the next row from the given result object, in associative array
@@ -798,43 +700,20 @@ class Database {
* @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;
- }
+ abstract function fetchRow( $res );
/**
* Get the number of rows in a result object
* @param $res Mixed: A SQL result
*/
- 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;
- }
+ abstract function numRows( $res );
/**
* Get the number of fields in a result object
* See documentation for mysql_num_fields()
* @param $res Mixed: A SQL result
*/
- function numFields( $res ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_num_fields( $res );
- }
+ abstract function numFields( $res );
/**
* Get a field name in a result object
@@ -843,12 +722,7 @@ class Database {
* @param $res Mixed: A SQL result
* @param $n Integer
*/
- function fieldName( $res, $n ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_field_name( $res, $n );
- }
+ abstract function fieldName( $res, $n );
/**
* Get the inserted value of an auto-increment row
@@ -860,7 +734,7 @@ class Database {
* $dbw->insert('page',array('page_id' => $id));
* $id = $dbw->insertId();
*/
- function insertId() { return mysql_insert_id( $this->mConn ); }
+ abstract function insertId();
/**
* Change the position of the cursor in a result object
@@ -868,51 +742,25 @@ class Database {
* @param $res Mixed: A SQL result
* @param $row Mixed: Either MySQL row or ResultWrapper
*/
- function dataSeek( $res, $row ) {
- if ( $res instanceof ResultWrapper ) {
- $res = $res->result;
- }
- return mysql_data_seek( $res, $row );
- }
+ abstract function dataSeek( $res, $row );
/**
* Get the last error number
* See mysql_errno()
*/
- function lastErrno() {
- if ( $this->mConn ) {
- return mysql_errno( $this->mConn );
- } else {
- return mysql_errno();
- }
- }
+ abstract function lastErrno();
/**
* 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;
- }
+ abstract function lastError();
+
/**
* 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 ); }
+ abstract function affectedRows();
/**
* Simple UPDATE wrapper
@@ -1095,7 +943,7 @@ class Database {
* e.g: selectRow( "page", array( "page_id" ), array( "page_namespace" =>
* NS_MAIN, "page_title" => "Astronomy" ) ) would return an object where
* $obj- >page_id is the ID of the Astronomy article
- * @param $fname String: Calling functio name
+ * @param $fname String: Calling function name
* @param $options Array
* @param $join_conds Array
*
@@ -1118,30 +966,27 @@ class Database {
/**
* Estimate rows in dataset
- * Returns estimated count, based on EXPLAIN output
+ * Returns estimated count - not necessarily an accurate estimate across different databases,
+ * so use sparingly
* 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
+ *
+ * @param string $table table name
+ * @param array $vars unused
+ * @param array $conds filters on the table
+ * @param string $fname function name for profiling
+ * @param array $options options for select
+ * @return int row count
+ */
+ public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
+ $rows = 0;
+ $res = $this->select ( $table, 'COUNT(*) AS rowcount', $conds, $fname, $options );
+ if ( $res ) {
+ $row = $this->fetchRow( $res );
+ $rows = ( isset( $row['rowcount'] ) ) ? $row['rowcount'] : 0;
}
-
- $this->freeResult($res);
- return $rows;
+ $this->freeResult( $res );
+ return $rows;
}
-
/**
* Removes most variables from an SQL query and replaces them with X or N for numbers.
@@ -1178,7 +1023,7 @@ class Database {
$table = $this->tableName( $table );
$res = $this->query( 'DESCRIBE '.$table, $fname );
if ( !$res ) {
- return NULL;
+ return null;
}
$found = false;
@@ -1200,7 +1045,7 @@ class Database {
function indexExists( $table, $index, $fname = 'Database::indexExists' ) {
$info = $this->indexInfo( $table, $index, $fname );
if ( is_null( $info ) ) {
- return NULL;
+ return null;
} else {
return $info !== false;
}
@@ -1220,7 +1065,7 @@ class Database {
$sql = 'SHOW INDEX FROM '.$table;
$res = $this->query( $sql, $fname );
if ( !$res ) {
- return NULL;
+ return null;
}
$result = array();
@@ -1257,18 +1102,7 @@ class Database {
* @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;
- }
+ abstract function fieldInfo( $table, $field );
/**
* mysql_field_type() wrapper
@@ -1286,7 +1120,7 @@ class Database {
function indexUnique( $table, $index ) {
$indexInfo = $this->indexInfo( $table, $index );
if ( !$indexInfo ) {
- return NULL;
+ return null;
}
return !$indexInfo[0]->Non_unique;
}
@@ -1440,11 +1274,32 @@ class Database {
}
/**
+ * Bitwise operations
+ */
+
+ function bitNot($field) {
+ return "(~$bitField)";
+ }
+
+ function bitAnd($fieldLeft, $fieldRight) {
+ return "($fieldLeft & $fieldRight)";
+ }
+
+ function bitOr($fieldLeft, $fieldRight) {
+ return "($fieldLeft | $fieldRight)";
+ }
+
+ /**
* Change the current database
+ *
+ * @return bool Success or failure
*/
function selectDB( $db ) {
- $this->mDBname = $db;
- return mysql_select_db( $db, $this->mConn );
+ # Stub. Shouldn't cause serious problems if it's not overridden, but
+ # if your database engine supports a concept similar to MySQL's
+ # databases you may as well. TODO: explain what exactly will fail if
+ # this is not overridden.
+ return true;
}
/**
@@ -1621,9 +1476,7 @@ class Database {
* @param $s String: to be slashed.
* @return String: slashed string.
*/
- function strencode( $s ) {
- return mysql_real_escape_string( $s, $this->mConn );
- }
+ abstract function strencode( $s );
/**
* If it's a string, adds quotes and backslashes
@@ -1642,30 +1495,78 @@ class Database {
}
/**
- * Escape string for safe LIKE usage
+ * Escape string for safe LIKE usage.
+ * WARNING: you should almost never use this function directly,
+ * instead use buildLike() that escapes everything automatically
*/
function escapeLike( $s ) {
- $s=str_replace('\\','\\\\',$s);
- $s=$this->strencode( $s );
- $s=str_replace(array('%','_'),array('\%','\_'),$s);
+ $s = str_replace( '\\', '\\\\', $s );
+ $s = $this->strencode( $s );
+ $s = str_replace( array( '%', '_' ), array( '\%', '\_' ), $s );
return $s;
}
/**
+ * LIKE statement wrapper, receives a variable-length argument list with parts of pattern to match
+ * containing either string literals that will be escaped or tokens returned by anyChar() or anyString().
+ * Alternatively, the function could be provided with an array of aforementioned parameters.
+ *
+ * Example: $dbr->buildLike( 'My_page_title/', $dbr->anyString() ) returns a LIKE clause that searches
+ * for subpages of 'My page title'.
+ * Alternatively: $pattern = array( 'My_page_title/', $dbr->anyString() ); $query .= $dbr->buildLike( $pattern );
+ *
+ * @ return String: fully built LIKE statement
+ */
+ function buildLike() {
+ $params = func_get_args();
+ if (count($params) > 0 && is_array($params[0])) {
+ $params = $params[0];
+ }
+
+ $s = '';
+ foreach( $params as $value) {
+ if( $value instanceof LikeMatch ) {
+ $s .= $value->toString();
+ } else {
+ $s .= $this->escapeLike( $value );
+ }
+ }
+ return " LIKE '" . $s . "' ";
+ }
+
+ /**
+ * Returns a token for buildLike() that denotes a '_' to be used in a LIKE query
+ */
+ function anyChar() {
+ return new LikeMatch( '_' );
+ }
+
+ /**
+ * Returns a token for buildLike() that denotes a '%' to be used in a LIKE query
+ */
+ function anyString() {
+ return new LikeMatch( '%' );
+ }
+
+ /**
* 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;
+ return null;
}
/**
- * USE INDEX clause
- * PostgreSQL doesn't have them and returns ""
+ * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
+ * is only needed because a) MySQL must be as efficient as possible due to
+ * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
+ * which index to pick. Anyway, other databases might have different
+ * indexes on a given table. So don't bother overriding this unless you're
+ * MySQL.
*/
function useIndexClause( $index ) {
- return "FORCE INDEX (" . $this->indexName( $index ) . ")";
+ return '';
}
/**
@@ -1753,10 +1654,14 @@ class Database {
}
/**
+ * A string to insert into queries to show that they're low-priority, like
+ * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
+ * string and nothing bad should happen.
+ *
* @return string Returns the text of the low priority option if it is supported, or a blank string otherwise
*/
function lowPriorityOption() {
- return 'LOW_PRIORITY';
+ return '';
}
/**
@@ -1810,27 +1715,60 @@ class Database {
}
/**
- * Construct a LIMIT query with optional offset
- * This is used for query pages
+ * Construct a LIMIT query with optional offset. This is used for query
+ * pages. The SQL should be adjusted so that only the first $limit rows
+ * are returned. If $offset is provided as well, then the first $offset
+ * rows should be discarded, and the next $limit rows should be returned.
+ * If the result of the query is not ordered, then the rows to be returned
+ * are theoretically arbitrary.
+ *
+ * $sql is expected to be a SELECT, if that makes a difference. For
+ * UPDATE, limitResultForUpdate should be used.
+ *
+ * The version provided by default works in MySQL and SQLite. It will very
+ * likely need to be overridden for most other DBMSes.
+ *
* @param $sql String: SQL query we will append the limit too
* @param $limit Integer: the SQL limit
* @param $offset Integer the SQL offset (default false)
*/
- function limitResult($sql, $limit, $offset=false) {
- if( !is_numeric($limit) ) {
+ 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);
+ function limitResultForUpdate( $sql, $num ) {
+ return $this->limitResult( $sql, $num, 0 );
}
/**
- * Returns an SQL expression for a simple conditional.
- * Uses IF on MySQL.
+ * Returns true if current database backend supports ORDER BY or LIMIT for separate subqueries
+ * within the UNION construct.
+ * @return Boolean
+ */
+ function unionSupportsOrderAndLimit() {
+ return true; // True for almost every DB supported
+ }
+
+ /**
+ * Construct a UNION query
+ * This is used for providing overload point for other DB abstractions
+ * not compatible with the MySQL syntax.
+ * @param $sqls Array: SQL statements to combine
+ * @param $all Boolean: use UNION ALL
+ * @return String: SQL fragment
+ */
+ function unionQueries($sqls, $all) {
+ $glue = $all ? ') UNION ALL (' : ') UNION (';
+ return '('.implode( $glue, $sqls ) . ')';
+ }
+
+ /**
+ * Returns an SQL expression for a simple conditional. This doesn't need
+ * to be overridden unless CASE isn't supported in your DBMS.
*
* @param $cond String: SQL expression which will result in a boolean value
* @param $trueVal String: SQL expression to return if true
@@ -1838,7 +1776,7 @@ class Database {
* @return String: SQL fragment
*/
function conditional( $cond, $trueVal, $falseVal ) {
- return " IF($cond, $trueVal, $falseVal) ";
+ return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
}
/**
@@ -1855,17 +1793,27 @@ class Database {
/**
* Determines if the last failure was due to a deadlock
+ * STUB
*/
function wasDeadlock() {
- return $this->lastErrno() == 1213;
+ return false;
}
/**
* Determines if the last query error was something that should be dealt
- * with by pinging the connection and reissuing the query
+ * with by pinging the connection and reissuing the query.
+ * STUB
*/
function wasErrorReissuable() {
- return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+ return false;
+ }
+
+ /**
+ * Determines if the last failure was due to the database being read-only.
+ * STUB
+ */
+ function wasReadOnlyError() {
+ return false;
}
/**
@@ -1935,7 +1883,7 @@ class Database {
# Commit any open transactions
if ( $this->mTrxLevel ) {
- $this->immediateCommit();
+ $this->commit();
}
if ( !is_null( $this->mFakeSlaveLag ) ) {
@@ -2048,6 +1996,21 @@ class Database {
}
/**
+ * Creates a new table with structure copied from existing table
+ * Note that unlike most database abstraction functions, this function does not
+ * automatically append database prefix, because it works at a lower
+ * abstraction level.
+ *
+ * @param $oldName String: name of table whose structure should be copied
+ * @param $newName String: name of table to be created
+ * @param $temporary Boolean: whether the new table should be temporary
+ * @return Boolean: true if operation was successful
+ */
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'Database::duplicateTableStructure' ) {
+ throw new MWException( 'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
+ }
+
+ /**
* Return MW-style timestamp used for MySQL schema
*/
function timestamp( $ts=0 ) {
@@ -2089,41 +2052,31 @@ class Database {
}
/**
+ * Returns a wikitext link to the DB's website, e.g.,
+ * return "[http://www.mysql.com/ MySQL]";
+ * Should at least contain plain text, if for some reason
+ * your database has no website.
+ *
* @return String: wikitext of a link to the server software's web site
*/
- function getSoftwareLink() {
- return "[http://www.mysql.com/ MySQL]";
- }
+ abstract function getSoftwareLink();
/**
+ * A string describing the current software version, like from
+ * mysql_get_server_info(). Will be listed on Special:Version, etc.
+ *
* @return String: Version information from the database
*/
- function getServerVersion() {
- return mysql_get_server_info( $this->mConn );
- }
+ abstract function getServerVersion();
/**
* Ping the server and try to reconnect if it there is no connection
+ *
+ * @return bool Success or failure
*/
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;
+ # Stub. Not essential to override.
+ return true;
}
/**
@@ -2135,7 +2088,7 @@ class Database {
wfDebug( "getLag: fake slave lagged {$this->mFakeSlaveLag} seconds\n" );
return $this->mFakeSlaveLag;
}
- $res = $this->query( 'SHOW PROCESSLIST' );
+ $res = $this->query( 'SHOW PROCESSLIST', __METHOD__ );
# Find slave SQL thread
while ( $row = $this->fetchObject( $res ) ) {
/* This should work for most situations - when default db
@@ -2149,7 +2102,10 @@ class Database {
$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'
+ $row->State != 'Requesting binlog dump' &&
+ $row->State != 'Waiting to reconnect after a failed master event read' &&
+ $row->State != 'Reconnecting after a failed master event read' &&
+ $row->State != 'Registering slave on master'
) {
# This is it, return the time (except -ve)
if ( $row->Time > 0x7fffffff ) {
@@ -2190,16 +2146,14 @@ class Database {
}
/**
- * 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.
+ * 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. May or may not be necessary for non-MySQL
+ * databases. For most purposes, leaving it as a no-op should be fine.
+ *
* @param $timeout Integer in seconds
*/
- public function setTimeout( $timeout ) {
- $this->query( "SET net_read_timeout=$timeout" );
- $this->query( "SET net_write_timeout=$timeout" );
- }
+ public function setTimeout( $timeout ) {}
/**
* Read and execute SQL commands from a file.
@@ -2211,14 +2165,45 @@ class Database {
function sourceFile( $filename, $lineCallback = false, $resultCallback = false ) {
$fp = fopen( $filename, 'r' );
if ( false === $fp ) {
- throw new MWException( "Could not open \"{$filename}\".\n" );
+ if (!defined("MEDIAWIKI_INSTALL"))
+ throw new MWException( "Could not open \"{$filename}\".\n" );
+ else
+ return "Could not open \"{$filename}\".\n";
+ }
+ try {
+ $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
}
- $error = $this->sourceStream( $fp, $lineCallback, $resultCallback );
+ catch( MWException $e ) {
+ if ( defined("MEDIAWIKI_INSTALL") ) {
+ $error = $e->getMessage();
+ } else {
+ fclose( $fp );
+ throw $e;
+ }
+ }
+
fclose( $fp );
return $error;
}
/**
+ * Get the full path of a patch file. Originally based on archive()
+ * from updaters.inc. Keep in mind this always returns a patch, as
+ * it fails back to MySQL if no DB-specific patch can be found
+ *
+ * @param $patch String The name of the patch, like patch-something.sql
+ * @return String Full path to patch file
+ */
+ public static function patchPath( $patch ) {
+ global $wgDBtype, $IP;
+ if ( file_exists( "$IP/maintenance/$wgDBtype/archives/$patch" ) ) {
+ return "$IP/maintenance/$wgDBtype/archives/$patch";
+ } else {
+ return "$IP/maintenance/archives/$patch";
+ }
+ }
+
+ /**
* 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 $fp String: File handle
@@ -2257,7 +2242,7 @@ class Database {
}
}
- if ( '' != $cmd ) { $cmd .= ' '; }
+ if ( $cmd != '' ) { $cmd .= ' '; }
$cmd .= "$line\n";
if ( $done ) {
@@ -2326,15 +2311,17 @@ class Database {
return $this->indexName( $matches[1] );
}
- /*
+ /**
* Build a concatenation list to feed into a SQL query
- */
+ * @param $stringList Array: list of raw SQL expressions; caller is responsible for any quoting
+ * @return String
+ */
function buildConcat( $stringList ) {
return 'CONCAT(' . implode( ',', $stringList ) . ')';
}
/**
- * Acquire a lock
+ * Acquire a named lock
*
* Abstracted from Filestore::lock() so child classes can implement for
* their own needs.
@@ -2343,32 +2330,44 @@ class Database {
* @param $method String: 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;
- }
+ public function lock( $lockName, $method, $timeout = 5 ) {
+ return true;
}
+
/**
* Release a lock.
*
- * @todo fixme - Figure out a way to return a bool
- * based on successful lock release.
- *
* @param $lockName String: Name of lock to release
* @param $method String: Name of method calling us
+ *
+ * FROM MYSQL DOCS: http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
+ * @return Returns 1 if the lock was released, 0 if the lock was not established
+ * by this thread (in which case the lock is not released), and NULL if the named
+ * lock did not exist
*/
public function unlock( $lockName, $method ) {
- $lockName = $this->addQuotes( $lockName );
- $result = $this->query( "SELECT RELEASE_LOCK($lockName)", $method );
- $this->freeResult( $result );
+ return true;
+ }
+
+ /**
+ * Lock specific tables
+ *
+ * @param $read Array of tables to lock for read access
+ * @param $write Array of tables to lock for write access
+ * @param $method String name of caller
+ * @param $lowPriority bool Whether to indicate writes to be LOW PRIORITY
+ */
+ public function lockTables( $read, $write, $method, $lowPriority = true ) {
+ return true;
+ }
+
+ /**
+ * Unlock specific tables
+ *
+ * @param $method String the caller
+ */
+ public function unlockTables( $method ) {
+ return true;
}
/**
@@ -2380,19 +2379,21 @@ class Database {
public function getSearchEngine() {
return "SearchMySQL";
}
-}
-/**
- * Database abstraction object for mySQL
- * Inherit all methods and properties of Database::Database()
- *
- * @ingroup Database
- * @see Database
- */
-class DatabaseMysql extends Database {
- # Inherit all
+ /**
+ * Allow or deny "big selects" for this session only. This is done by setting
+ * the sql_big_selects session variable.
+ *
+ * This is a MySQL-specific feature.
+ *
+ * @param mixed $value true for allow, false for deny, or "default" to restore the initial value
+ */
+ public function setBigSelects( $value = true ) {
+ // no-op
+ }
}
+
/******************************************************************************
* Utility classes
*****************************************************************************/
@@ -2502,10 +2503,19 @@ class DBError extends MWException {
* @param $db Database object which threw the error
* @param $error A simple error message to be used for debugging
*/
- function __construct( Database &$db, $error ) {
+ function __construct( DatabaseBase &$db, $error ) {
$this->db =& $db;
parent::__construct( $error );
}
+
+ function getText() {
+ global $wgShowDBErrorBacktrace;
+ $s = $this->getMessage() . "\n";
+ if ( $wgShowDBErrorBacktrace ) {
+ $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
+ }
+ return $s;
+ }
}
/**
@@ -2514,7 +2524,7 @@ class DBError extends MWException {
class DBConnectionError extends DBError {
public $error;
- function __construct( Database &$db, $error = 'unknown error' ) {
+ function __construct( DatabaseBase &$db, $error = 'unknown error' ) {
$msg = 'DB connection error';
if ( trim( $error ) != '' ) {
$msg .= ": $error";
@@ -2533,10 +2543,6 @@ class DBConnectionError extends DBError {
return false;
}
- function getText() {
- return $this->getMessage() . "\n";
- }
-
function getLogMessage() {
# Don't send to the exception log
return false;
@@ -2553,7 +2559,7 @@ class DBConnectionError extends DBError {
}
function getHTML() {
- global $wgLang, $wgMessageCache, $wgUseFileCache;
+ global $wgLang, $wgMessageCache, $wgUseFileCache, $wgShowDBErrorBacktrace;
$sorry = 'Sorry! This site is experiencing technical difficulties.';
$again = 'Try waiting a few minutes and reloading.';
@@ -2577,30 +2583,31 @@ class DBConnectionError extends DBError {
$noconnect = "<p><strong>$sorry</strong><br />$again</p><p><small>$info</small></p>";
$text = str_replace( '$1', $this->error, $noconnect );
- /*
- if ( $GLOBALS['wgShowExceptionDetails'] ) {
- $text .= '</p><p>Backtrace:</p><p>' .
- nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
- "</p>\n";
- }*/
+ if ( $wgShowDBErrorBacktrace ) {
+ $text .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+ }
$extra = $this->searchForm();
if( $wgUseFileCache ) {
- $cache = $this->fileCachedPage();
- # Cached version on file system?
- if( $cache !== null ) {
- # Hack: extend the body for error messages
- $cache = str_replace( array('</html>','</body>'), '', $cache );
- # Add cache notice...
- $cachederror = "This is a cached copy of the requested page, and may not be up to date. ";
- # Localize it if possible...
- if( $wgLang instanceof Language ) {
- $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) );
+ try {
+ $cache = $this->fileCachedPage();
+ # Cached version on file system?
+ if( $cache !== null ) {
+ # Hack: extend the body for error messages
+ $cache = str_replace( array('</html>','</body>'), '', $cache );
+ # Add cache notice...
+ $cachederror = "This is a cached copy of the requested page, and may not be up to date. ";
+ # Localize it if possible...
+ if( $wgLang instanceof Language ) {
+ $cachederror = htmlspecialchars( $wgLang->getMessage( 'dberr-cachederror' ) );
+ }
+ $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>";
+ # Output cached page with notices on bottom and re-close body
+ return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>";
}
- $warning = "<div style='color:red;font-size:150%;font-weight:bold;'>$cachederror</div>";
- # Output cached page with notices on bottom and re-close body
- return "{$cache}{$warning}<hr />$text<hr />$extra</body></html>";
+ } catch( MWException $e ) {
+ // Do nothing, just use the default page
}
}
# Headers needed here - output is just the error message
@@ -2631,8 +2638,6 @@ class DBConnectionError extends DBError {
<input type="hidden" name="ie" value="$wgInputEncoding" />
<input type="hidden" name="oe" value="$wgInputEncoding" />
- <img src="http://www.google.com/logos/Logo_40wht.gif" alt="" style="float:left; margin-left: 1.5em; margin-right: 1.5em;" />
-
<input type="text" name="q" size="31" maxlength="255" value="$search" />
<input type="submit" name="btnG" value="$googlesearch" />
<div>
@@ -2653,9 +2658,9 @@ EOT;
$mainpage = htmlspecialchars( $wgLang->getMessage( 'mainpage' ) );
}
- if($wgTitle) {
+ if( $wgTitle ) {
$t =& $wgTitle;
- } elseif($title) {
+ } elseif( $title ) {
$t = Title::newFromURL( $title );
} else {
$t = Title::newFromText( $mainpage );
@@ -2681,7 +2686,7 @@ EOT;
class DBQueryError extends DBError {
public $error, $errno, $sql, $fname;
- function __construct( Database &$db, $error, $errno, $sql, $fname ) {
+ function __construct( DatabaseBase &$db, $error, $errno, $sql, $fname ) {
$message = "A database error has occurred\n" .
"Query: $sql\n" .
"Function: $fname\n" .
@@ -2695,11 +2700,16 @@ class DBQueryError extends DBError {
}
function getText() {
+ global $wgShowDBErrorBacktrace;
if ( $this->useMessageCache() ) {
- return wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
- htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ $s = wfMsg( 'dberrortextcl', htmlspecialchars( $this->getSQL() ),
+ htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) ) . "\n";
+ if ( $wgShowDBErrorBacktrace ) {
+ $s .= "Backtrace:\n" . $this->getTraceAsString() . "\n";
+ }
+ return $s;
} else {
- return $this->getMessage();
+ return parent::getText();
}
}
@@ -2722,12 +2732,17 @@ class DBQueryError extends DBError {
}
function getHTML() {
+ global $wgShowDBErrorBacktrace;
if ( $this->useMessageCache() ) {
- return wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
+ $s = wfMsgNoDB( 'dberrortext', htmlspecialchars( $this->getSQL() ),
htmlspecialchars( $this->fname ), $this->errno, htmlspecialchars( $this->error ) );
} else {
- return nl2br( htmlspecialchars( $this->getMessage() ) );
+ $s = nl2br( htmlspecialchars( $this->getMessage() ) );
}
+ if ( $wgShowDBErrorBacktrace ) {
+ $s .= '<p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) );
+ }
+ return $s;
}
}
@@ -2841,15 +2856,18 @@ class ResultWrapper implements Iterator {
}
}
-class MySQLMasterPos {
- var $file, $pos;
+/**
+ * Used by DatabaseBase::buildLike() to represent characters that have special meaning in SQL LIKE clauses
+ * and thus need no escaping. Don't instantiate it manually, use Database::anyChar() and anyString() instead.
+ */
+class LikeMatch {
+ private $str;
- function __construct( $file, $pos ) {
- $this->file = $file;
- $this->pos = $pos;
+ public function __construct( $s ) {
+ $this->str = $s;
}
- function __toString() {
- return "{$this->file}/{$this->pos}";
+ public function toString() {
+ return $this->str;
}
}
diff --git a/includes/db/DatabaseIbm_db2.php b/includes/db/DatabaseIbm_db2.php
index fcd0bc2d..9b62af82 100644
--- a/includes/db/DatabaseIbm_db2.php
+++ b/includes/db/DatabaseIbm_db2.php
@@ -9,37 +9,33 @@
*/
/**
- * Utility class for generating blank objects
- * Intended as an equivalent to {} in Javascript
- * @ingroup Database
- */
-class BlankObject {
-}
-
-/**
* This represents a column in a DB2 database
* @ingroup Database
*/
class IBM_DB2Field {
- private $name, $tablename, $type, $nullable, $max_length;
+ private $name = '';
+ private $tablename = '';
+ private $type = '';
+ private $nullable = false;
+ private $max_length = 0;
/**
* Builder method for the class
- * @param Object $db Database interface
- * @param string $table table name
- * @param string $field column name
+ * @param $db DatabaseIbm_db2: Database interface
+ * @param $table String: table name
+ * @param $field String: column name
* @return IBM_DB2Field
*/
static function fromText($db, $table, $field) {
global $wgDBmwschema;
- $q = <<<END
+ $q = <<<SQL
SELECT
lcase(coltype) AS typname,
nulls AS attnotnull, length AS attlen
FROM sysibm.syscolumns
WHERE tbcreator=%s AND tbname=%s AND name=%s;
-END;
+SQL;
$res = $db->query(sprintf($q,
$db->addQuotes($wgDBmwschema),
$db->addQuotes($table),
@@ -89,20 +85,25 @@ END;
class IBM_DB2Blob {
private $mData;
- function __construct($data) {
+ public function __construct($data) {
$this->mData = $data;
}
- function getData() {
+ public function getData() {
return $this->mData;
}
+
+ public function __toString()
+ {
+ return $this->mData;
+ }
}
/**
* Primary database interface
* @ingroup Database
*/
-class DatabaseIbm_db2 extends Database {
+class DatabaseIbm_db2 extends DatabaseBase {
/*
* Inherited members
protected $mLastQuery = '';
@@ -122,27 +123,42 @@ class DatabaseIbm_db2 extends Database {
*/
/// Server port for uncataloged connections
- protected $mPort = NULL;
+ protected $mPort = null;
/// Whether connection is cataloged
- protected $mCataloged = NULL;
+ protected $mCataloged = null;
/// Schema for tables, stored procedures, triggers
- protected $mSchema = NULL;
+ protected $mSchema = null;
/// Whether the schema has been applied in this session
protected $mSchemaSet = false;
/// Result of last query
- protected $mLastResult = NULL;
+ protected $mLastResult = null;
/// Number of rows affected by last INSERT/UPDATE/DELETE
- protected $mAffectedRows = NULL;
+ protected $mAffectedRows = null;
/// Number of rows returned by last SELECT
- protected $mNumRows = NULL;
+ protected $mNumRows = null;
+
+ /// Connection config options - see constructor
+ public $mConnOptions = array();
+ /// Statement config options -- see constructor
+ public $mStmtOptions = array();
const CATALOGED = "cataloged";
const UNCATALOGED = "uncataloged";
const USE_GLOBAL = "get from global";
+ const NONE_OPTION = 0x00;
+ const CONN_OPTION = 0x01;
+ const STMT_OPTION = 0x02;
+
+ const REGULAR_MODE = 'regular';
+ const INSTALL_MODE = 'install';
+
+ // Whether this is regular operation or the initial installation
+ protected $mMode = self::REGULAR_MODE;
+
/// Last sequence value used for a primary key
- protected $mInsertId = NULL;
+ protected $mInsertId = null;
/*
* These can be safely inherited
@@ -219,7 +235,7 @@ class DatabaseIbm_db2 extends Database {
*/
/*
- * These need to be implemented TODO
+ * These have been implemented
*
* Administrative: 7 / 7
* constructor [Done]
@@ -375,7 +391,10 @@ class DatabaseIbm_db2 extends Database {
return $this->mDBname;
}
}
-
+
+ function getType() {
+ return 'ibm_db2';
+ }
######################################
# Setup
@@ -384,12 +403,13 @@ class DatabaseIbm_db2 extends Database {
/**
*
- * @param string $server hostname of database server
- * @param string $user username
- * @param string $password
- * @param string $dbName database name on the server
- * @param function $failFunction (optional)
- * @param integer $flags database behaviour flags (optional, unused)
+ * @param $server String: hostname of database server
+ * @param $user String: username
+ * @param $password String: password
+ * @param $dbName String: database name on the server
+ * @param $failFunction Callback (optional)
+ * @param $flags Integer: database behaviour flags (optional, unused)
+ * @param $schema String
*/
public function DatabaseIbm_db2($server = false, $user = false, $password = false,
$dbName = false, $failFunction = false, $flags = 0,
@@ -399,7 +419,7 @@ class DatabaseIbm_db2 extends Database {
global $wgOut, $wgDBmwschema;
# Can't get a reference if it hasn't been set yet
if ( !isset( $wgOut ) ) {
- $wgOut = NULL;
+ $wgOut = null;
}
$this->mOut =& $wgOut;
$this->mFailFunction = $failFunction;
@@ -412,17 +432,50 @@ class DatabaseIbm_db2 extends Database {
$this->mSchema = $schema;
}
+ // configure the connection and statement objects
+ $this->setDB2Option('db2_attr_case', 'DB2_CASE_LOWER', self::CONN_OPTION | self::STMT_OPTION);
+ $this->setDB2Option('deferred_prepare', 'DB2_DEFERRED_PREPARE_ON', self::STMT_OPTION);
+ $this->setDB2Option('rowcount', 'DB2_ROWCOUNT_PREFETCH_ON', self::STMT_OPTION);
+
$this->open( $server, $user, $password, $dbName);
}
/**
+ * Enables options only if the ibm_db2 extension version supports them
+ * @param $name String: name of the option in the options array
+ * @param $const String: name of the constant holding the right option value
+ * @param $type Integer: whether this is a Connection or Statement otion
+ */
+ private function setDB2Option($name, $const, $type) {
+ if (defined($const)) {
+ if ($type & self::CONN_OPTION) $this->mConnOptions[$name] = constant($const);
+ if ($type & self::STMT_OPTION) $this->mStmtOptions[$name] = constant($const);
+ }
+ else {
+ $this->installPrint("$const is not defined. ibm_db2 version is likely too low.");
+ }
+ }
+
+ /**
+ * Outputs debug information in the appropriate place
+ * @param $string String: the relevant debug message
+ */
+ private function installPrint($string) {
+ wfDebug("$string");
+ if ($this->mMode == self::INSTALL_MODE) {
+ print "<li>$string</li>";
+ flush();
+ }
+ }
+
+ /**
* Opens a database connection and returns it
* Closes any existing connection
* @return a fresh connection
- * @param string $server hostname
- * @param string $user
- * @param string $password
- * @param string $dbName database name
+ * @param $server String: hostname
+ * @param $user String
+ * @param $password String
+ * @param $dbName String: database name
*/
public function open( $server, $user, $password, $dbName )
{
@@ -437,7 +490,7 @@ class DatabaseIbm_db2 extends Database {
// Test for IBM DB2 support, to avoid suppressed fatal error
if ( !function_exists( 'db2_connect' ) ) {
$error = "DB2 functions missing, have you enabled the ibm_db2 extension for PHP?\n";
- wfDebug($error);
+ $this->installPrint($error);
$this->reportConnectionError($error);
}
@@ -461,16 +514,16 @@ class DatabaseIbm_db2 extends Database {
elseif ( $cataloged == self::UNCATALOGED ) {
$this->openUncataloged($dbName, $user, $password, $server, $port);
}
- // Don't do this
+ // Apply connection config
+ db2_set_option($this->mConn, $this->mConnOptions, 1);
// Not all MediaWiki code is transactional
- // Rather, turn it off in the begin function and turn on after a commit
- // db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF);
+ // Rather, turn autocommit off in the begin function and turn on after a commit
db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
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" );
+ $this->installPrint( "DB connection error\n" );
+ $this->installPrint( "Server: $server, Database: $dbName, User: $user, Password: " . substr( $password, 0, 3 ) . "...\n" );
+ $this->installPrint( $this->lastError()."\n" );
return null;
}
@@ -524,14 +577,14 @@ class DatabaseIbm_db2 extends Database {
/**
* Returns a fresh instance of this class
- * @static
- * @return
- * @param string $server hostname of database server
- * @param string $user username
- * @param string $password
- * @param string $dbName database name on the server
- * @param function $failFunction (optional)
- * @param integer $flags database behaviour flags (optional, unused)
+ *
+ * @param $server String: hostname of database server
+ * @param $user String: username
+ * @param $password String
+ * @param $dbName String: database name on the server
+ * @param $failFunction Callback (optional)
+ * @param $flags Integer: database behaviour flags (optional, unused)
+ * @return DatabaseIbm_db2 object
*/
static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0)
{
@@ -543,20 +596,16 @@ class DatabaseIbm_db2 extends Database {
* Forces a database rollback
*/
public function lastError() {
- if ($this->lastError2()) {
- $this->rollback();
- return true;
- }
- return false;
- }
-
- private function lastError2() {
$connerr = db2_conn_errormsg();
- if ($connerr) return $connerr;
+ if ($connerr) {
+ //$this->rollback();
+ return $connerr;
+ }
$stmterr = db2_stmt_errormsg();
- if ($stmterr) return $stmterr;
- if ($this->mConn) return "No open connection.";
- if ($this->mOpened) return "No open connection allegedly.";
+ if ($stmterr) {
+ //$this->rollback();
+ return $stmterr;
+ }
return false;
}
@@ -592,7 +641,7 @@ class DatabaseIbm_db2 extends Database {
// Switch into the correct namespace
$this->applySchema();
- $ret = db2_exec( $this->mConn, $sql );
+ $ret = db2_exec( $this->mConn, $sql, $this->mStmtOptions );
if( !$ret ) {
print "<br><pre>";
print $sql;
@@ -601,7 +650,7 @@ class DatabaseIbm_db2 extends Database {
throw new DBUnexpectedError($this, 'SQL error: ' . htmlspecialchars( $error ) );
}
$this->mLastResult = $ret;
- $this->mAffectedRows = NULL; // Not calculated until asked for
+ $this->mAffectedRows = null; // Not calculated until asked for
return $ret;
}
@@ -653,17 +702,6 @@ EOF;
if( $this->lastErrno() ) {
throw new DBUnexpectedError( $this, 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) );
}
- // Make field names lowercase for compatibility with MySQL
- if ($row)
- {
- $row2 = new BlankObject();
- foreach ($row as $key => $value)
- {
- $keyu = strtolower($key);
- $row2->$keyu = $value;
- }
- $row = $row2;
- }
return $row;
}
@@ -707,14 +745,26 @@ EOF;
$this->applySchema();
$this->begin();
- $res = dbsource( "../maintenance/ibm_db2/tables.sql", $this);
+ $res = $this->sourceFile( "../maintenance/ibm_db2/tables.sql" );
+ if ($res !== true) {
+ print " <b>FAILED</b>: " . htmlspecialchars( $res ) . "</li>";
+ } else {
+ print " done</li>";
+ }
$res = null;
// TODO: update mediawiki_version table
// TODO: populate interwiki links
- $this->commit();
+ if ($this->lastError()) {
+ print "<li>Errors encountered during table creation -- rolled back</li>\n";
+ print "<li>Please install again</li>\n";
+ $this->rollback();
+ }
+ else {
+ $this->commit();
+ }
}
catch (MWException $mwe)
{
@@ -725,15 +775,17 @@ EOF;
/**
* Escapes strings
* Doesn't escape numbers
- * @param string s string to escape
+ * @param $s String: string to escape
* @return escaped string
*/
public function addQuotes( $s ) {
- //wfDebug("DB2::addQuotes($s)\n");
+ //$this->installPrint("DB2::addQuotes($s)\n");
if ( is_null( $s ) ) {
return "NULL";
} else if ($s instanceof Blob) {
return "'".$s->fetch($s)."'";
+ } else if ($s instanceof IBM_DB2Blob) {
+ return "'".$this->decodeBlob($s)."'";
}
$s = $this->strencode($s);
if ( is_numeric($s) ) {
@@ -745,41 +797,9 @@ EOF;
}
/**
- * Escapes strings
- * Only escapes numbers going into non-numeric fields
- * @param string s string to escape
- * @return escaped string
- */
- public function addQuotesSmart( $table, $field, $s ) {
- if ( is_null( $s ) ) {
- return "NULL";
- } else if ($s instanceof Blob) {
- return "'".$s->fetch($s)."'";
- }
- $s = $this->strencode($s);
- if ( is_numeric($s) ) {
- // Check with the database if the column is actually numeric
- // This allows for numbers in titles, etc
- $res = $this->doQuery("SELECT $field FROM $table FETCH FIRST 1 ROWS ONLY");
- $type = db2_field_type($res, strtoupper($field));
- if ( $this->is_numeric_type( $type ) ) {
- //wfDebug("DB2: Numeric value going in a numeric column: $s in $type $field in $table\n");
- return $s;
- }
- else {
- wfDebug("DB2: Numeric in non-numeric: '$s' in $type $field in $table\n");
- return "'$s'";
- }
- }
- else {
- return "'$s'";
- }
- }
-
- /**
* Verifies that a DB2 column/field type is numeric
* @return bool true if numeric
- * @param string $type DB2 column type
+ * @param $type String: DB2 column type
*/
public function is_numeric_type( $type ) {
switch (strtoupper($type)) {
@@ -798,7 +818,7 @@ EOF;
/**
* Alias for addQuotes()
- * @param string s string to escape
+ * @param $s String: string to escape
* @return escaped string
*/
public function strencode( $s ) {
@@ -830,7 +850,7 @@ EOF;
/**
* Start a transaction (mandatory)
*/
- public function begin() {
+ public function begin( $fname = 'DatabaseIbm_db2::begin' ) {
// turn off auto-commit
db2_autocommit($this->mConn, DB2_AUTOCOMMIT_OFF);
$this->mTrxLevel = 1;
@@ -840,7 +860,7 @@ EOF;
* End a transaction
* Must have a preceding begin()
*/
- public function commit() {
+ public function commit( $fname = 'DatabaseIbm_db2::commit' ) {
db2_commit($this->mConn);
// turn auto-commit back on
db2_autocommit($this->mConn, DB2_AUTOCOMMIT_ON);
@@ -850,7 +870,7 @@ EOF;
/**
* Cancel a transaction
*/
- public function rollback() {
+ public function rollback( $fname = 'DatabaseIbm_db2::rollback' ) {
db2_rollback($this->mConn);
// turn auto-commit back on
// not sure if this is appropriate
@@ -868,7 +888,6 @@ EOF;
* LIST_NAMES - comma separated field names
*/
public function makeList( $a, $mode = LIST_COMMA ) {
- wfDebug("DB2::makeList()\n");
if ( !is_array( $a ) ) {
throw new DBUnexpectedError( $this, 'Database::makeList called with incorrect parameters' );
}
@@ -931,88 +950,18 @@ EOF;
}
/**
- * Makes an encoded list of strings from an array
- * Quotes numeric values being inserted into non-numeric fields
- * @return string
- * @param string $table name of the table
- * @param array $a list of values
- * @param $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
- */
- public function makeListSmart( $table, $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 = ";
- }
- if ( $mode == LIST_NAMES ) {
- $list .= $value;
- }
- else {
- $list .= $this->addQuotesSmart( $table, $field, $value );
- }
- }
- }
- return $list;
- }
-
- /**
* Construct a LIMIT query with optional offset
* This is used for query pages
- * $sql string SQL query we will append the limit too
- * $limit integer the SQL limit
- * $offset integer the SQL offset (default false)
+ * @param $sql string SQL query we will append the limit too
+ * @param $limit integer the SQL limit
+ * @param $offset integer the SQL offset (default false)
*/
public function limitResult($sql, $limit, $offset=false) {
if( !is_numeric($limit) ) {
throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
}
if( $offset ) {
- wfDebug("Offset parameter not supported in limitResult()\n");
+ $this->installPrint("Offset parameter not supported in limitResult()\n");
}
// TODO implement proper offset handling
// idea: get all the rows between 0 and offset, advance cursor to offset
@@ -1026,20 +975,22 @@ EOF;
*/
public function tableName( $name ) {
# Replace reserved words with better ones
- switch( $name ) {
- case 'user':
- return 'mwuser';
- case 'text':
- return 'pagecontent';
- default:
- return $name;
- }
+// switch( $name ) {
+// case 'user':
+// return 'mwuser';
+// case 'text':
+// return 'pagecontent';
+// default:
+// return $name;
+// }
+ // we want maximum compatibility with MySQL schema
+ return $name;
}
/**
* Generates a timestamp in an insertable format
* @return string timestamp value
- * @param timestamp $ts
+ * @param $ts timestamp
*/
public function timestamp( $ts=0 ) {
// TS_MW cannot be easily distinguished from an integer
@@ -1048,16 +999,21 @@ EOF;
/**
* Return the next in a sequence, save the value for retrieval via insertId()
- * @param string seqName Name of a defined sequence in the database
+ * @param $seqName String: name of a defined sequence in the database
* @return next value in that sequence
*/
public function nextSequenceValue( $seqName ) {
+ // Not using sequences in the primary schema to allow for easy third-party migration scripts
+ // Emulating MySQL behaviour of using NULL to signal that sequences aren't used
+ /*
$safeseq = preg_replace( "/'/", "''", $seqName );
$res = $this->query( "VALUES NEXTVAL FOR $safeseq" );
$row = $this->fetchRow( $res );
$this->mInsertId = $row[0];
$this->freeResult( $res );
return $this->mInsertId;
+ */
+ return null;
}
/**
@@ -1069,139 +1025,180 @@ EOF;
}
/**
+ * Updates the mInsertId property with the value of the last insert into a generated column
+ * @param $table String: sanitized table name
+ * @param $primaryKey Mixed: string name of the primary key or a bool if this call is a do-nothing
+ * @param $stmt Resource: prepared statement resource
+ * of the SELECT primary_key FROM FINAL TABLE ( INSERT ... ) form
+ */
+ private function calcInsertId($table, $primaryKey, $stmt) {
+ if ($primaryKey) {
+ $id_row = $this->fetchRow($stmt);
+ $this->mInsertId = $id_row[0];
+ }
+ }
+
+ /**
* 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
*
- * @param array $table String: Name of the table to insert to.
- * @param array $args Array: Items to insert into the table.
- * @param array $fname String: Name of the function, for profiling
- * @param mixed $options String or Array. Valid options: IGNORE
+ * @param $table String: Name of the table to insert to.
+ * @param $args Array: Items to insert into the table.
+ * @param $fname String: Name of the function, for profiling
+ * @param $options String or Array. Valid options: IGNORE
*
* @return bool Success of insert operation. IGNORE always returns true.
*/
public function insert( $table, $args, $fname = 'DatabaseIbm_db2::insert', $options = array() ) {
- wfDebug("DB2::insert($table)\n");
if ( !count( $args ) ) {
return true;
}
-
+ // get database-specific table name (not used)
$table = $this->tableName( $table );
-
- if ( !is_array( $options ) )
- $options = array( $options );
-
- if ( isset( $args[0] ) && is_array( $args[0] ) ) {
- }
- else {
+ // format options as an array
+ if ( !is_array( $options ) ) $options = array( $options );
+ // format args as an array of arrays
+ if ( !( isset( $args[0] ) && is_array( $args[0] ) ) ) {
$args = array($args);
}
+ // prevent insertion of NULL into primary key columns
+ list($args, $primaryKeys) = $this->removeNullPrimaryKeys($table, $args);
+ // if there's only one primary key
+ // we'll be able to read its value after insertion
+ $primaryKey = false;
+ if (count($primaryKeys) == 1) {
+ $primaryKey = $primaryKeys[0];
+ }
+
+ // get column names
$keys = array_keys( $args[0] );
+ $key_count = count($keys);
// If IGNORE is set, we use savepoints to emulate mysql's behavior
$ignore = in_array( 'IGNORE', $options ) ? 'mw' : '';
-
- // Cache autocommit value at the start
- $oldautocommit = db2_autocommit($this->mConn);
+ // assume success
+ $res = true;
// If we are not in a transaction, we need to be for savepoint trickery
$didbegin = 0;
if (! $this->mTrxLevel) {
$this->begin();
$didbegin = 1;
}
- if ( $ignore ) {
- $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 ';
+ switch($key_count) {
+ //case 0 impossible
+ case 1:
+ $sql .= '(?)';
+ break;
+ default:
+ $sql .= '(?' . str_repeat(',?', $key_count-1) . ')';
+ }
+ // add logic to read back the new primary key value
+ if ($primaryKey) {
+ $sql = "SELECT $primaryKey FROM FINAL TABLE($sql)";
+ }
+ $stmt = $this->prepare($sql);
+
+ // start a transaction/enter transaction mode
+ $this->begin();
if ( !$ignore ) {
$first = true;
foreach ( $args as $row ) {
- if ( $first ) {
- $first = false;
- } else {
- $sql .= ',';
- }
- $sql .= '(' . $this->makeListSmart( $table, $row ) . ')';
+ // insert each row into the database
+ $res = $res & $this->execute($stmt, $row);
+ // get the last inserted value into a generated column
+ $this->calcInsertId($table, $primaryKey, $stmt);
}
- $res = (bool)$this->query( $sql, $fname, $ignore );
}
else {
+ $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;
+
+ // always return true
$res = true;
- $origsql = $sql;
+
foreach ( $args as $row ) {
- $tempsql = $origsql;
- $tempsql .= '(' . $this->makeListSmart( $table, $row ) . ')';
-
- if ( $ignore ) {
- db2_exec($this->mConn, "SAVEPOINT $ignore");
+ $overhead = "SAVEPOINT $ignore ON ROLLBACK RETAIN CURSORS";
+ db2_exec($this->mConn, $overhead, $this->mStmtOptions);
+
+ $res2 = $this->execute($stmt, $row);
+ // get the last inserted value into a generated column
+ $this->calcInsertId($table, $primaryKey, $stmt);
+
+ $errNum = $this->lastErrno();
+ if ($errNum) {
+ db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore", $this->mStmtOptions );
}
-
- $tempres = (bool)$this->query( $tempsql, $fname, $ignore );
-
- if ( $ignore ) {
- $bar = db2_stmt_error();
- if ($bar != false) {
- db2_exec( $this->mConn, "ROLLBACK TO SAVEPOINT $ignore" );
- }
- else {
- db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore" );
- $numrowsinserted++;
- }
+ else {
+ db2_exec( $this->mConn, "RELEASE SAVEPOINT $ignore", $this->mStmtOptions );
+ $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;
}
- }
-
- if ($didbegin) {
- $this->commit();
- }
- // if autocommit used to be on, it's ok to commit everything
- else if ($oldautocommit)
- {
- $this->commit();
- }
-
- if ( $ignore ) {
+
$olde = error_reporting( $olde );
// Set the affected row count for the whole operation
$this->mAffectedRows = $numrowsinserted;
-
- // IGNORE always returns true
- return true;
}
+ // commit either way
+ $this->commit();
return $res;
}
/**
+ * Given a table name and a hash of columns with values
+ * Removes primary key columns from the hash where the value is NULL
+ *
+ * @param $table String: name of the table
+ * @param $args Array of hashes of column names with values
+ * @return Array: tuple containing filtered array of columns, array of primary keys
+ */
+ private function removeNullPrimaryKeys($table, $args) {
+ $schema = $this->mSchema;
+ // find out the primary keys
+ $keyres = db2_primary_keys($this->mConn, null, strtoupper($schema), strtoupper($table));
+ $keys = array();
+ for ($row = $this->fetchObject($keyres); $row != null; $row = $this->fetchRow($keyres)) {
+ $keys[] = strtolower($row->column_name);
+ }
+ // remove primary keys
+ foreach ($args as $ai => $row) {
+ foreach ($keys as $ki => $key) {
+ if ($row[$key] == null) {
+ unset($row[$key]);
+ }
+ }
+ $args[$ai] = $row;
+ }
+ // return modified hash
+ return array($args, $keys);
+ }
+
+ /**
* 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() ) {
+ * @param $table String: The table to UPDATE
+ * @param $values An array of values to SET
+ * @param $conds An array of conditions (WHERE). Use '*' to update all rows.
+ * @param $fname String: The Class::Function calling this function
+ * (for the log)
+ * @param $options An array of UPDATE options, can be one or
+ * more of IGNORE, LOW_PRIORITY
+ * @return Boolean
+ */
+ public function update( $table, $values, $conds, $fname = 'Database::update', $options = array() ) {
$table = $this->tableName( $table );
$opts = $this->makeUpdateOptions( $options );
- $sql = "UPDATE $opts $table SET " . $this->makeListSmart( $table, $values, LIST_SET );
+ $sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
if ( $conds != '*' ) {
- $sql .= " WHERE " . $this->makeListSmart( $table, $conds, LIST_AND );
+ $sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
}
return $this->query( $sql, $fname );
}
@@ -1211,21 +1208,21 @@ EOF;
*
* Use $conds == "*" to delete all rows
*/
- function delete( $table, $conds, $fname = 'Database::delete' ) {
+ public 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->makeListSmart( $table, $conds, LIST_AND );
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
}
return $this->query( $sql, $fname );
}
/**
* Returns the number of rows affected by the last query or 0
- * @return int the number of rows affected by the last query
+ * @return Integer: the number of rows affected by the last query
*/
public function affectedRows() {
if ( !is_null( $this->mAffectedRows ) ) {
@@ -1238,20 +1235,11 @@ EOF;
}
/**
- * USE INDEX clause
- * DB2 doesn't have them and returns ""
- * @param sting $index
- */
- public function useIndexClause( $index ) {
- return "";
- }
-
- /**
* Simulates REPLACE with a DELETE followed by INSERT
* @param $table Object
- * @param array $uniqueIndexes array consisting of indexes and arrays of indexes
- * @param array $rows Rows to insert
- * @param string $fname Name of the function for profiling
+ * @param $uniqueIndexes Array consisting of indexes and arrays of indexes
+ * @param $rows Array: rows to insert
+ * @param $fname String: name of the function for profiling
* @return nothing
*/
function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseIbm_db2::replace' ) {
@@ -1306,8 +1294,8 @@ EOF;
/**
* Returns the number of rows in the result set
* Has to be called right after the corresponding select query
- * @param Object $res result set
- * @return int number of rows
+ * @param $res Object result set
+ * @return Integer: number of rows
*/
public function numRows( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -1323,8 +1311,8 @@ EOF;
/**
* Moves the row pointer of the result set
- * @param Object $res result set
- * @param int $row row number
+ * @param $res Object: result set
+ * @param $row Integer: row number
* @return success or failure
*/
public function dataSeek( $res, $row ) {
@@ -1340,8 +1328,8 @@ EOF;
/**
* Frees memory associated with a statement resource
- * @param Object $res Statement resource to free
- * @return bool success or failure
+ * @param $res Object: statement resource to free
+ * @return Boolean success or failure
*/
public function freeResult( $res ) {
if ( $res instanceof ResultWrapper ) {
@@ -1354,7 +1342,7 @@ EOF;
/**
* Returns the number of columns in a resource
- * @param Object $res Statement resource
+ * @param $res Object: statement resource
* @return Number of fields/columns in resource
*/
public function numFields( $res ) {
@@ -1366,9 +1354,9 @@ EOF;
/**
* Returns the nth column name
- * @param Object $res Statement resource
- * @param int $n Index of field or column
- * @return string name of nth column
+ * @param $res Object: statement resource
+ * @param $n Integer: Index of field or column
+ * @return String name of nth column
*/
public function fieldName( $res, $n ) {
if ( $res instanceof ResultWrapper ) {
@@ -1380,15 +1368,15 @@ EOF;
/**
* 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
+ * @param $table Array or string, table name(s) (prefix auto-added)
+ * @param $vars Array or string, field name(s) to be retrieved
+ * @param $conds Array or string, condition(s) for WHERE
+ * @param $fname String: calling function name (use __METHOD__) for logs/profiling
+ * @param $options Associative array of options (e.g. array('GROUP BY' => 'page_title')),
+ * see Database::makeSelectOptions code for list of supported stuff
+ * @param $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
*/
public function select( $table, $vars, $conds='', $fname = 'DatabaseIbm_db2::select', $options = array(), $join_conds = array() )
{
@@ -1419,7 +1407,6 @@ EOF;
$obj = $this->fetchObject($res2);
$this->mNumRows = $obj->num_rows;
- wfDebug("DatabaseIbm_db2::select: There are $this->mNumRows rows.\n");
return $res;
}
@@ -1430,9 +1417,9 @@ EOF;
*
* @private
*
- * @param array $options an associative array of options to be turned into
+ * @param $options Associative array of options to be turned into
* an SQL query, valid keys are listed in the function.
- * @return array
+ * @return Array
*/
function makeSelectOptions( $options ) {
$preLimitTail = $postLimitTail = '';
@@ -1463,46 +1450,18 @@ EOF;
}
/**
- * Does nothing
- * @param object $db
- * @return bool true
- */
- public function selectDB( $db ) {
- return true;
- }
-
- /**
- * Returns an SQL expression for a simple conditional.
- * Uses CASE on DB2
- *
- * @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
- */
- public function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- ###
- # Fix search crash
- ###
- /**
* Get search engine class. All subclasses of this
* need to implement this if they wish to use searching.
*
- * @return string
+ * @return String
*/
public function getSearchEngine() {
return "SearchIBM_DB2";
}
-
- ###
- # Tuesday the 14th of October, 2008
- ###
+
/**
* Did the last database access fail because of deadlock?
- * @return bool
+ * @return Boolean
*/
public function wasDeadlock() {
// get SQLSTATE
@@ -1511,7 +1470,7 @@ EOF;
case '40001': // sql0911n, Deadlock or timeout, rollback
case '57011': // sql0904n, Resource unavailable, no rollback
case '57033': // sql0913n, Deadlock or timeout, no rollback
- wfDebug("In a deadlock because of SQLSTATE $err");
+ $this->installPrint("In a deadlock because of SQLSTATE $err");
return true;
}
return false;
@@ -1520,13 +1479,13 @@ EOF;
/**
* Ping the server and try to reconnect if it there is no connection
* The connection may be closed and reopened while this happens
- * @return bool whether the connection exists
+ * @return Boolean: whether the connection exists
*/
public function ping() {
// db2_ping() doesn't exist
// Emulate
$this->close();
- if ($this->mCataloged == NULL) {
+ if ($this->mCataloged == null) {
return false;
}
else if ($this->mCataloged) {
@@ -1545,46 +1504,34 @@ EOF;
* @return string ''
* @deprecated
*/
- public function getStatus( $which ) { wfDebug('Not implemented for DB2: getStatus()'); return ''; }
- /**
- * Not implemented
- * @deprecated
- */
- public function setTimeout( $timeout ) { wfDebug('Not implemented for DB2: setTimeout()'); }
+ public function getStatus( $which="%" ) { $this->installPrint('Not implemented for DB2: getStatus()'); return ''; }
/**
* Not implemented
* TODO
* @return bool true
*/
- public function lock( $lockName, $method ) { wfDebug('Not implemented for DB2: lock()'); return true; }
- /**
- * Not implemented
- * TODO
- * @return bool true
- */
- public function unlock( $lockName, $method ) { wfDebug('Not implemented for DB2: unlock()'); return true; }
/**
* Not implemented
* @deprecated
*/
- public function setFakeSlaveLag( $lag ) { wfDebug('Not implemented for DB2: setFakeSlaveLag()'); }
+ public function setFakeSlaveLag( $lag ) { $this->installPrint('Not implemented for DB2: setFakeSlaveLag()'); }
/**
* Not implemented
* @deprecated
*/
- public function setFakeMaster( $enabled ) { wfDebug('Not implemented for DB2: setFakeMaster()'); }
+ public function setFakeMaster( $enabled = true ) { $this->installPrint('Not implemented for DB2: setFakeMaster()'); }
/**
* Not implemented
* @return string $sql
* @deprecated
*/
- public function limitResultForUpdate($sql, $num) { return $sql; }
+ public function limitResultForUpdate($sql, $num) { $this->installPrint('Not implemented for DB2: limitResultForUpdate()'); return $sql; }
+
/**
- * No such option
- * @return string ''
- * @deprecated
+ * Only useful with fake prepare like in base Database class
+ * @return string
*/
- public function lowPriorityOption() { return ''; }
+ public function fillPreparedArg( $matches ) { $this->installPrint('Not useful for DB2: fillPreparedArg()'); return ''; }
######################################
# Reflection
@@ -1592,9 +1539,9 @@ EOF;
/**
* Query whether a given column exists in the mediawiki schema
- * @param string $table name of the table
- * @param string $field name of the column
- * @param string $fname function name for logging and profiling
+ * @param $table String: name of the table
+ * @param $field String: name of the column
+ * @param $fname String: function name for logging and profiling
*/
public function fieldExists( $table, $field, $fname = 'DatabaseIbm_db2::fieldExists' ) {
$table = $this->tableName( $table );
@@ -1617,10 +1564,10 @@ SQL;
/**
* Returns information about an index
* If errors are explicitly ignored, returns NULL on failure
- * @param string $table table name
- * @param string $index index name
- * @param string
- * @return object query row in object form
+ * @param $table String: table name
+ * @param $index String: index name
+ * @param $fname String: function name for logging and profiling
+ * @return Object query row in object form
*/
public function indexInfo( $table, $index, $fname = 'DatabaseIbm_db2::indexExists' ) {
$table = $this->tableName( $table );
@@ -1631,17 +1578,17 @@ WHERE si.name='$index' AND si.tbname='$table' AND sc.tbcreator='$this->mSchema'
SQL;
$res = $this->query( $sql, $fname );
if ( !$res ) {
- return NULL;
+ return null;
}
$row = $this->fetchObject( $res );
- if ($row != NULL) return $row;
+ if ($row != null) return $row;
else return false;
}
/**
* Returns an information object on a table column
- * @param string $table table name
- * @param string $field column name
+ * @param $table String: table name
+ * @param $field String: column name
* @return IBM_DB2Field
*/
public function fieldInfo( $table, $field ) {
@@ -1650,9 +1597,9 @@ SQL;
/**
* db2_field_type() wrapper
- * @param Object $res Result of executed statement
- * @param mixed $index number or name of the column
- * @return string column type
+ * @param $res Object: result of executed statement
+ * @param $index Mixed: number or name of the column
+ * @return String column type
*/
public function fieldType( $res, $index ) {
if ( $res instanceof ResultWrapper ) {
@@ -1663,10 +1610,10 @@ SQL;
/**
* Verifies that an index was created as unique
- * @param string $table table name
- * @param string $index index name
- * @param string $fnam function name for profiling
- * @return bool
+ * @param $table String: table name
+ * @param $index String: index name
+ * @param $fname function name for profiling
+ * @return Bool
*/
public function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
$table = $this->tableName( $table );
@@ -1689,9 +1636,9 @@ SQL;
/**
* Returns the size of a text field, or -1 for "unlimited"
- * @param string $table table name
- * @param string $field column name
- * @return int length or -1 for unlimited
+ * @param $table String: table name
+ * @param $field String: column name
+ * @return Integer: length or -1 for unlimited
*/
public function textFieldSize( $table, $field ) {
$table = $this->tableName( $table );
@@ -1709,12 +1656,12 @@ SQL;
/**
* DELETE where the condition is a join
- * @param string $delTable deleting from this table
- * @param string $joinTable using data from this table
- * @param string $delVar variable in deleteable table
- * @param string $joinVar variable in data table
- * @param array $conds conditionals for join table
- * @param string $fname function name for profiling
+ * @param $delTable String: deleting from this table
+ * @param $joinTable String: using data from this table
+ * @param $delVar String: variable in deleteable table
+ * @param $joinVar String: variable in data table
+ * @param $conds Array: conditionals for join table
+ * @param $fname String: function name for profiling
*/
public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseIbm_db2::deleteJoin" ) {
if ( !$conds ) {
@@ -1731,32 +1678,10 @@ SQL;
$this->query( $sql, $fname );
}
-
- /**
- * Estimate rows in dataset
- * Returns estimated count, based on COUNT(*) output
- * Takes same arguments as Database::select()
- * @param string $table table name
- * @param array $vars unused
- * @param array $conds filters on the table
- * @param string $fname function name for profiling
- * @param array $options options for select
- * @return int row count
- */
- public function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) {
- $rows = 0;
- $res = $this->select ($table, 'COUNT(*) as mwrowcount', $conds, $fname, $options );
- if ($res) {
- $row = $this->fetchRow($res);
- $rows = (isset($row['mwrowcount'])) ? $row['mwrowcount'] : 0;
- }
- $this->freeResult($res);
- return $rows;
- }
-
+
/**
* Description is left as an exercise for the reader
- * @param mixed $b data to be encoded
+ * @param $b Mixed: data to be encoded
* @return IBM_DB2Blob
*/
public function encodeBlob($b) {
@@ -1765,7 +1690,7 @@ SQL;
/**
* Description is left as an exercise for the reader
- * @param IBM_DB2Blob $b data to be decoded
+ * @param $b IBM_DB2Blob: data to be decoded
* @return mixed
*/
public function decodeBlob($b) {
@@ -1774,8 +1699,8 @@ SQL;
/**
* Convert into a list of string being concatenated
- * @param array $stringList strings that need to be joined together by the SQL engine
- * @return string joined by the concatenation operator
+ * @param $stringList Array: strings that need to be joined together by the SQL engine
+ * @return String: joined by the concatenation operator
*/
public function buildConcat( $stringList ) {
// || is equivalent to CONCAT
@@ -1785,12 +1710,135 @@ SQL;
/**
* Generates the SQL required to convert a DB2 timestamp into a Unix epoch
- * @param string $column name of timestamp column
- * @return string SQL code
+ * @param $column String: name of timestamp column
+ * @return String: SQL code
*/
public function extractUnixEpoch( $column ) {
// TODO
// see SpecialAncientpages
}
+
+ ######################################
+ # Prepared statements
+ ######################################
+
+ /**
+ * 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...)
+ * @param $sql String: SQL statement with appropriate markers
+ * @param $func String: Name of the function, for profiling
+ * @return resource a prepared DB2 SQL statement
+ */
+ public function prepare( $sql, $func = 'DB2::prepare' ) {
+ $stmt = db2_prepare($this->mConn, $sql, $this->mStmtOptions);
+ return $stmt;
+ }
+
+ /**
+ * Frees resources associated with a prepared statement
+ * @return Boolean success or failure
+ */
+ public function freePrepared( $prepared ) {
+ return db2_free_stmt($prepared);
+ }
+
+ /**
+ * Execute a prepared query with the various arguments
+ * @param $prepared String: the prepared sql
+ * @param $args Mixed: either an array here, or put scalars as varargs
+ * @return Resource: results object
+ */
+ public function execute( $prepared, $args = null ) {
+ if( !is_array( $args ) ) {
+ # Pull the var args
+ $args = func_get_args();
+ array_shift( $args );
+ }
+ $res = db2_execute($prepared, $args);
+ return $res;
+ }
+
+ /**
+ * Prepare & execute an SQL statement, quoting and inserting arguments
+ * in the appropriate places.
+ * @param $query String
+ * @param $args ...
+ */
+ public function safeQuery( $query, $args = null ) {
+ // copied verbatim from Database.php
+ $prepared = $this->prepare( $query, 'DB2::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 $preparedQuery String: a 'preparable' SQL statement
+ * @param $args Array of arguments to fill it with
+ * @return String: executable statement
+ */
+ public function fillPrepared( $preparedQuery, $args ) {
+ reset( $args );
+ $this->preparedArgs =& $args;
+
+ foreach ($args as $i => $arg) {
+ db2_bind_param($preparedQuery, $i+1, $args[$i]);
+ }
+
+ return $preparedQuery;
+ }
+
+ /**
+ * Switches module between regular and install modes
+ */
+ public function setMode($mode) {
+ $old = $this->mMode;
+ $this->mMode = $mode;
+ return $old;
+ }
+
+ /**
+ * Bitwise negation of a column or value in SQL
+ * Same as (~field) in C
+ * @param $field String
+ * @return String
+ */
+ function bitNot($field) {
+ //expecting bit-fields smaller than 4bytes
+ return 'BITNOT('.$bitField.')';
+ }
+
+ /**
+ * Bitwise AND of two columns or values in SQL
+ * Same as (fieldLeft & fieldRight) in C
+ * @param $fieldLeft String
+ * @param $fieldRight String
+ * @return String
+ */
+ function bitAnd($fieldLeft, $fieldRight) {
+ return 'BITAND('.$fieldLeft.', '.$fieldRight.')';
+ }
+
+ /**
+ * Bitwise OR of two columns or values in SQL
+ * Same as (fieldLeft | fieldRight) in C
+ * @param $fieldLeft String
+ * @param $fieldRight String
+ * @return String
+ */
+ function bitOr($fieldLeft, $fieldRight) {
+ return 'BITOR('.$fieldLeft.', '.$fieldRight.')';
+ }
}
-?> \ No newline at end of file
diff --git a/includes/db/DatabaseMssql.php b/includes/db/DatabaseMssql.php
index 28ccab2d..6b1206b0 100644
--- a/includes/db/DatabaseMssql.php
+++ b/includes/db/DatabaseMssql.php
@@ -10,7 +10,7 @@
/**
* @ingroup Database
*/
-class DatabaseMssql extends Database {
+class DatabaseMssql extends DatabaseBase {
var $mAffectedRows;
var $mLastResult;
@@ -25,7 +25,7 @@ class DatabaseMssql extends Database {
$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
+ 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;
@@ -45,6 +45,10 @@ class DatabaseMssql extends Database {
}
+ function getType() {
+ return 'mssql';
+ }
+
/**
* todo: check if these should be true like parent class
*/
@@ -131,7 +135,7 @@ class DatabaseMssql extends Database {
function close() {
$this->mOpened = false;
if ($this->mConn) {
- if ($this->trxLevel()) $this->immediateCommit();
+ if ($this->trxLevel()) $this->commit();
return mssql_close($this->mConn);
} else return true;
}
@@ -446,22 +450,6 @@ class DatabaseMssql extends Database {
}
/**
- * 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
@@ -490,13 +478,13 @@ class DatabaseMssql extends Database {
function indexInfo( $table, $index, $fname = 'Database::indexInfo' ) {
throw new DBUnexpectedError( $this, 'Database::indexInfo called which is not supported yet' );
- return NULL;
+ return null;
$table = $this->tableName( $table );
$sql = 'SHOW INDEX FROM '.$table;
$res = $this->query( $sql, $fname );
if ( !$res ) {
- return NULL;
+ return null;
}
$result = array();
@@ -708,13 +696,6 @@ class DatabaseMssql extends Database {
}
/**
- * 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
@@ -858,18 +839,6 @@ class DatabaseMssql extends Database {
}
/**
- * Returns an SQL expression for a simple conditional.
- *
- * @param $cond String: SQL expression which will result in a boolean value
- * @param $trueVal String: SQL expression to return if true
- * @param $falseVal String: SQL expression to return if false
- * @return string SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
- /**
* Should determine if the last failure was due to a deadlock
* @return bool
*/
@@ -878,22 +847,6 @@ class DatabaseMssql extends Database {
}
/**
- * 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 ) {
@@ -931,16 +884,6 @@ class DatabaseMssql extends Database {
}
/**
- * 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() {
@@ -1001,20 +944,9 @@ class DatabaseMssql extends Database {
}
}
- /**
- * No-op lock functions
- */
- public function lock( $lockName, $method ) {
- return true;
- }
- public function unlock( $lockName, $method ) {
- return true;
- }
-
public function getSearchEngine() {
return "SearchEngineDummy";
}
-
}
/**
diff --git a/includes/db/DatabaseMysql.php b/includes/db/DatabaseMysql.php
new file mode 100644
index 00000000..ea7ef5b9
--- /dev/null
+++ b/includes/db/DatabaseMysql.php
@@ -0,0 +1,453 @@
+<?php
+/**
+ * Database abstraction object for mySQL
+ * Inherit all methods and properties of Database::Database()
+ *
+ * @ingroup Database
+ * @see Database
+ */
+class DatabaseMysql extends DatabaseBase {
+ function getType() {
+ return 'mysql';
+ }
+
+ /*private*/ function doQuery( $sql ) {
+ if( $this->bufferResults() ) {
+ $ret = mysql_query( $sql, $this->mConn );
+ } else {
+ $ret = mysql_unbuffered_query( $sql, $this->mConn );
+ }
+ return $ret;
+ }
+
+ function open( $server, $user, $password, $dbName ) {
+ global $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");
+
+ # The kernel's default SYN retransmission period is far too slow for us,
+ # so we use a short timeout plus a manual retry. Retrying means that a small
+ # but finite rate of SYN packet loss won't cause user-visible errors.
+ $this->mConn = false;
+ if ( ini_get( 'mysql.connect_timeout' ) <= 3 ) {
+ $numAttempts = 2;
+ } else {
+ $numAttempts = 1;
+ }
+ $this->installErrorHandler();
+ for ( $i = 0; $i < $numAttempts && !$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();
+ # Always log connection errors
+ if ( !$this->mConn ) {
+ $error = $this->lastError();
+ if ( !$error ) {
+ $error = $phpError;
+ }
+ wfLogDBError( "Error connecting to {$this->mServer}: $error\n" );
+ wfDebug( "DB connection error\n" );
+ wfDebug( "Server: $server, User: $user, Password: " .
+ substr( $password, 0, 3 ) . "..., error: " . mysql_error() . "\n" );
+ $success = false;
+ }
+
+ wfProfileOut("dbconnect-$server");
+
+ if ( $dbName != '' && $this->mConn !== false ) {
+ $success = @/**/mysql_select_db( $dbName, $this->mConn );
+ if ( !$success ) {
+ $error = "Error selecting database $dbName on server {$this->mServer} " .
+ "from client host " . wfHostname() . "\n";
+ wfLogDBError(" Error selecting database $dbName on server {$this->mServer} \n");
+ wfDebug( $error );
+ }
+ } else {
+ # Delay USE query
+ $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;
+ }
+
+ function close() {
+ $this->mOpened = false;
+ if ( $this->mConn ) {
+ if ( $this->trxLevel() ) {
+ $this->commit();
+ }
+ return mysql_close( $this->mConn );
+ } else {
+ return true;
+ }
+ }
+
+ function freeResult( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ if ( !@/**/mysql_free_result( $res ) ) {
+ throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
+ }
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ function numFields( $res ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_num_fields( $res );
+ }
+
+ function fieldName( $res, $n ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_field_name( $res, $n );
+ }
+
+ function insertId() { return mysql_insert_id( $this->mConn ); }
+
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ResultWrapper ) {
+ $res = $res->result;
+ }
+ return mysql_data_seek( $res, $row );
+ }
+
+ function lastErrno() {
+ if ( $this->mConn ) {
+ return mysql_errno( $this->mConn );
+ } else {
+ return mysql_errno();
+ }
+ }
+
+ 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;
+ }
+
+ function affectedRows() { return mysql_affected_rows( $this->mConn ); }
+
+ /**
+ * Estimate rows in dataset
+ * Returns estimated count, based on EXPLAIN output
+ * Takes same arguments as Database::select()
+ */
+ public 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;
+ }
+
+ 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;
+ }
+
+ function selectDB( $db ) {
+ $this->mDBname = $db;
+ return mysql_select_db( $db, $this->mConn );
+ }
+
+ function strencode( $s ) {
+ return mysql_real_escape_string( $s, $this->mConn );
+ }
+
+ 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;
+ }
+
+ function getServerVersion() {
+ return mysql_get_server_info( $this->mConn );
+ }
+
+ function useIndexClause( $index ) {
+ return "FORCE INDEX (" . $this->indexName( $index ) . ")";
+ }
+
+ function lowPriorityOption() {
+ return 'LOW_PRIORITY';
+ }
+
+ function getSoftwareLink() {
+ return '[http://www.mysql.com/ MySQL]';
+ }
+
+ function standardSelectDistinct() {
+ return false;
+ }
+
+ public function setTimeout( $timeout ) {
+ $this->query( "SET net_read_timeout=$timeout" );
+ $this->query( "SET net_write_timeout=$timeout" );
+ }
+
+ public function lock( $lockName, $method, $timeout = 5 ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT GET_LOCK($lockName, $timeout) 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;
+ }
+ }
+
+ public function unlock( $lockName, $method ) {
+ $lockName = $this->addQuotes( $lockName );
+ $result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
+ $row = $this->fetchObject( $result );
+ return $row->lockstatus;
+ }
+
+ public function lockTables( $read, $write, $method, $lowPriority = true ) {
+ $items = array();
+
+ foreach( $write as $table ) {
+ $tbl = $this->tableName( $table ) .
+ ( $lowPriority ? ' LOW_PRIORITY' : '' ) .
+ ' WRITE';
+ $items[] = $tbl;
+ }
+ foreach( $read as $table ) {
+ $items[] = $this->tableName( $table ) . ' READ';
+ }
+ $sql = "LOCK TABLES " . implode( ',', $items );
+ $this->query( $sql, $method );
+ }
+
+ public function unlockTables( $method ) {
+ $this->query( "UNLOCK TABLES", $method );
+ }
+
+ public function setBigSelects( $value = true ) {
+ if ( $value === 'default' ) {
+ if ( $this->mDefaultBigSelects === null ) {
+ # Function hasn't been called before so it must already be set to the default
+ return;
+ } else {
+ $value = $this->mDefaultBigSelects;
+ }
+ } elseif ( $this->mDefaultBigSelects === null ) {
+ $this->mDefaultBigSelects = (bool)$this->selectField( false, '@@sql_big_selects' );
+ }
+ $encValue = $value ? '1' : '0';
+ $this->query( "SET sql_big_selects=$encValue", __METHOD__ );
+ }
+
+
+ /**
+ * Determines if the last failure was due to a deadlock
+ */
+ function wasDeadlock() {
+ return $this->lastErrno() == 1213;
+ }
+
+ /**
+ * Determines if the last query error was something that should be dealt
+ * with by pinging the connection and reissuing the query
+ */
+ function wasErrorReissuable() {
+ return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
+ }
+
+ /**
+ * Determines if the last failure was due to the database being read-only.
+ */
+ function wasReadOnlyError() {
+ return $this->lastErrno() == 1223 ||
+ ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
+ }
+
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseMysql::duplicateTableStructure' ) {
+ $tmp = $temporary ? 'TEMPORARY ' : '';
+ if ( strcmp( $this->getServerVersion(), '4.1' ) < 0 ) {
+ # Hack for MySQL versions < 4.1, which don't support
+ # "CREATE TABLE ... LIKE". Note that
+ # "CREATE TEMPORARY TABLE ... SELECT * FROM ... LIMIT 0"
+ # would not create the indexes we need....
+ #
+ # Note that we don't bother changing around the prefixes here be-
+ # cause we know we're using MySQL anyway.
+
+ $res = $this->query( "SHOW CREATE TABLE $oldName" );
+ $row = $this->fetchRow( $res );
+ $oldQuery = $row[1];
+ $query = preg_replace( '/CREATE TABLE `(.*?)`/',
+ "CREATE $tmp TABLE `$newName`", $oldQuery );
+ if ($oldQuery === $query) {
+ # Couldn't do replacement
+ throw new MWException( "could not create temporary table $newName" );
+ }
+ } else {
+ $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
+ }
+ $this->query( $query, $fname );
+ }
+
+}
+
+/**
+ * Legacy support: Database == DatabaseMysql
+ */
+class Database extends DatabaseMysql {}
+
+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/DatabaseOracle.php b/includes/db/DatabaseOracle.php
index 4c37a507..bd60bbf8 100644
--- a/includes/db/DatabaseOracle.php
+++ b/includes/db/DatabaseOracle.php
@@ -11,7 +11,7 @@
class ORABlob {
var $mData;
- function __construct($data) {
+ function __construct( $data ) {
$this->mData = $data;
}
@@ -31,58 +31,80 @@ class ORAResult {
private $cursor;
private $stmt;
private $nrows;
- private $db;
- function __construct(&$db, $stmt) {
+ private $unique;
+ private function array_unique_md( $array_in ) {
+ $array_out = array();
+ $array_hashes = array();
+
+ foreach ( $array_in as $key => $item ) {
+ $hash = md5( serialize( $item ) );
+ if ( !isset( $array_hashes[$hash] ) ) {
+ $array_hashes[$hash] = $hash;
+ $array_out[] = $item;
+ }
+ }
+
+ return $array_out;
+ }
+
+ function __construct( &$db, $stmt, $unique = false ) {
$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__);
+
+ 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;
}
+ if ( $unique ) {
+ $this->rows = $this->array_unique_md( $this->rows );
+ $this->nrows = count( $this->rows );
+ }
+
$this->cursor = 0;
$this->stmt = $stmt;
}
- function free() {
- oci_free_statement($this->stmt);
+ public function free() {
+ oci_free_statement( $this->stmt );
}
- function seek($row) {
- $this->cursor = min($row, $this->nrows);
+ public function seek( $row ) {
+ $this->cursor = min( $row, $this->nrows );
}
- function numRows() {
+ public function numRows() {
return $this->nrows;
}
- function numFields() {
- return oci_num_fields($this->stmt);
+ public function numFields() {
+ return oci_num_fields( $this->stmt );
}
- function fetchObject() {
- if ($this->cursor >= $this->nrows)
+ public 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));
+ 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)
+ public function fetchRow() {
+ 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));
+ foreach ( $row as $k => $v ) {
+ $lc = strtolower( oci_field_name( $this->stmt, $k + 1 ) );
$ret[$lc] = $v;
$ret[$k] = $v;
}
@@ -91,30 +113,87 @@ class ORAResult {
}
/**
+ * Utility class.
+ * @ingroup Database
+ */
+class ORAField {
+ private $name, $tablename, $default, $max_length, $nullable,
+ $is_pk, $is_unique, $is_multiple, $is_key, $type;
+
+ function __construct( $info ) {
+ $this->name = $info['column_name'];
+ $this->tablename = $info['table_name'];
+ $this->default = $info['data_default'];
+ $this->max_length = $info['data_length'];
+ $this->nullable = $info['not_null'];
+ $this->is_pk = isset( $info['prim'] ) && $info['prim'] == 1 ? 1 : 0;
+ $this->is_unique = isset( $info['uniq'] ) && $info['uniq'] == 1 ? 1 : 0;
+ $this->is_multiple = isset( $info['nonuniq'] ) && $info['nonuniq'] == 1 ? 1 : 0;
+ $this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
+ $this->type = $info['data_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;
+ }
+}
+
+/**
* @ingroup Database
*/
-class DatabaseOracle extends Database {
- var $mInsertId = NULL;
- var $mLastResult = NULL;
- var $numeric_version = NULL;
+class DatabaseOracle extends DatabaseBase {
+ 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 )
- {
+ var $ignore_DUP_VAL_ON_INDEX = false;
+ var $sequenceData = null;
- 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);
+ var $defaultCharset = 'AL32UTF8';
+
+ var $mFieldInfoCache = array();
+
+ function __construct( $server = false, $user = false, $password = false, $dbName = false,
+ $failFunction = false, $flags = 0, $tablePrefix = 'get from global' )
+ {
+ $tablePrefix = $tablePrefix == 'get from global' ? $tablePrefix : strtoupper( $tablePrefix );
+ parent::__construct( $server, $user, $password, $dbName, $failFunction, $flags, $tablePrefix );
+ wfRunHooks( 'DatabaseOraclePostInit', array( &$this ) );
+ }
+ function getType() {
+ return 'oracle';
}
function cascadingDeletes() {
@@ -139,8 +218,7 @@ class DatabaseOracle extends Database {
return true;
}
- static function newFromParams( $server = false, $user = false, $password = false, $dbName = false,
- $failFunction = false, $flags = 0)
+ static function newFromParams( $server, $user, $password, $dbName, $failFunction = false, $flags = 0 )
{
return new DatabaseOracle( $server, $user, $password, $dbName, $failFunction, $flags );
}
@@ -154,30 +232,35 @@ class DatabaseOracle extends Database {
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
+ if ( !strlen( $user ) ) { # e.g. the class is being loaded
return;
}
- error_reporting( E_ALL );
- $this->mConn = oci_connect($user, $password, $dbName);
+ $session_mode = $this->mFlags & DBO_SYSDBA ? OCI_SYSDBA : OCI_DEFAULT;
+ if ( $this->mFlags & DBO_DEFAULT ) {
+ $this->mConn = oci_new_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode );
+ } else {
+ $this->mConn = oci_connect( $user, $password, $dbName, $this->defaultCharset, $session_mode );
+ }
- 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");
+ 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;
+
+ # removed putenv calls because they interfere with the system globaly
+ $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
+ $this->doQuery( 'ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT=\'DD-MM-YYYY HH24:MI:SS.FF6\'' );
return $this->mConn;
}
@@ -198,55 +281,101 @@ class DatabaseOracle extends Database {
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");
+ function doQuery( $sql ) {
+ wfDebug( "SQL: [$sql]\n" );
+ if ( !mb_check_encoding( $sql ) ) {
+ throw new MWException( "SQL encoding is invalid\n$sql" );
}
- if (($this->mLastResult = $stmt = oci_parse($this->mConn, $sql)) === false) {
- $e = oci_error($this->mConn);
- $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+ // handle some oracle specifics
+ // remove AS column/table/subquery namings
+ if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
+ $sql = preg_replace( '/ as /i', ' ', $sql );
}
+ // Oracle has issues with UNION clause if the statement includes LOB fields
+ // So we do a UNION ALL and then filter the results array with array_unique
+ $union_unique = ( preg_match( '/\/\* UNION_UNIQUE \*\/ /', $sql ) != 0 );
+ // EXPLAIN syntax in Oracle is EXPLAIN PLAN FOR and it return nothing
+ // you have to select data from plan table after explain
+ $explain_id = date( 'dmYHis' );
+
+ $sql = preg_replace( '/^EXPLAIN /', 'EXPLAIN PLAN SET STATEMENT_ID = \'' . $explain_id . '\' FOR', $sql, 1, $explain_count );
+
- if (oci_execute($stmt, $this->execFlags()) == false) {
- $e = oci_error($stmt);
- $this->reportQueryError($e['message'], $e['code'], $sql, __FUNCTION__);
+ wfSuppressWarnings();
+
+ if ( ( $this->mLastResult = $stmt = oci_parse( $this->mConn, $sql ) ) === false ) {
+ $e = oci_error( $this->mConn );
+ $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ );
+ return false;
+ }
+
+ if ( oci_execute( $stmt, $this->execFlags() ) == false ) {
+ $e = oci_error( $stmt );
+ if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) {
+ $this->reportQueryError( $e['message'], $e['code'], $sql, __FUNCTION__ );
+ return false;
+ }
}
- if (oci_statement_type($stmt) == "SELECT")
- return new ORAResult($this, $stmt);
- else {
- $this->mAffectedRows = oci_num_rows($stmt);
+
+ wfRestoreWarnings();
+
+ if ( $explain_count > 0 ) {
+ return $this->doQuery( 'SELECT id, cardinality "ROWS" FROM plan_table WHERE statement_id = \'' . $explain_id . '\'' );
+ } elseif ( oci_statement_type( $stmt ) == 'SELECT' ) {
+ return new ORAResult( $this, $stmt, $union_unique );
+ } else {
+ $this->mAffectedRows = oci_num_rows( $stmt );
return true;
}
}
- function queryIgnore($sql, $fname = '') {
- return $this->query($sql, $fname, true);
+ function queryIgnore( $sql, $fname = '' ) {
+ return $this->query( $sql, $fname, true );
}
- function freeResult($res) {
- $res->free();
+ function freeResult( $res ) {
+ if ( $res instanceof ORAResult ) {
+ $res->free();
+ } else {
+ $res->result->free();
+ }
}
- function fetchObject($res) {
- return $res->fetchObject();
+ function fetchObject( $res ) {
+ if ( $res instanceof ORAResult ) {
+ return $res->numRows();
+ } else {
+ return $res->result->fetchObject();
+ }
}
- function fetchRow($res) {
- return $res->fetchAssoc();
+ function fetchRow( $res ) {
+ if ( $res instanceof ORAResult ) {
+ return $res->fetchRow();
+ } else {
+ return $res->result->fetchRow();
+ }
}
- function numRows($res) {
- return $res->numRows();
+ function numRows( $res ) {
+ if ( $res instanceof ORAResult ) {
+ return $res->numRows();
+ } else {
+ return $res->result->numRows();
+ }
}
- function numFields($res) {
- return $res->numFields();
+ function numFields( $res ) {
+ if ( $res instanceof ORAResult ) {
+ return $res->numFields();
+ } else {
+ return $res->result->numFields();
+ }
}
- function fieldName($stmt, $n) {
- return pg_field_name($stmt, $n);
+ function fieldName( $stmt, $n ) {
+ return oci_field_name( $stmt, $n );
}
/**
@@ -256,23 +385,29 @@ class DatabaseOracle extends Database {
return $this->mInsertId;
}
- function dataSeek($res, $row) {
- $res->seek($row);
+ function dataSeek( $res, $row ) {
+ if ( $res instanceof ORAResult ) {
+ $res->seek( $row );
+ } else {
+ $res->result->seek( $row );
+ }
}
function lastError() {
- if ($this->mConn === false)
+ if ( $this->mConn === false ) {
$e = oci_error();
- else
- $e = oci_error($this->mConn);
+ } else {
+ $e = oci_error( $this->mConn );
+ }
return $e['message'];
}
function lastErrno() {
- if ($this->mConn === false)
+ if ( $this->mConn === false ) {
$e = oci_error();
- else
- $e = oci_error($this->mConn);
+ } else {
+ $e = oci_error( $this->mConn );
+ }
return $e['code'];
}
@@ -284,122 +419,261 @@ class DatabaseOracle extends Database {
* Returns information about an index
* If errors are explicitly ignored, returns NULL on failure
*/
- function indexInfo( $table, $index, $fname = 'Database::indexExists' ) {
+ function indexInfo( $table, $index, $fname = 'DatabaseOracle::indexExists' ) {
return false;
}
- function indexUnique ($table, $index, $fname = 'Database::indexUnique' ) {
+ function indexUnique( $table, $index, $fname = 'DatabaseOracle::indexUnique' ) {
return false;
}
- function insert( $table, $a, $fname = 'Database::insert', $options = array() ) {
- if (!is_array($options))
- $options = array($options);
+ function insert( $table, $a, $fname = 'DatabaseOracle::insert', $options = array() ) {
+ if ( !count( $a ) ) {
+ return true;
+ }
+
+ if ( !is_array( $options ) ) {
+ $options = array( $options );
+ }
- #if (in_array('IGNORE', $options))
- # $oldIgnore = $this->ignoreErrors(true);
+ if ( in_array( 'IGNORE', $options ) ) {
+ $this->ignore_DUP_VAL_ON_INDEX = 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);
+ if ( !is_array( reset( $a ) ) ) {
+ $a = array( $a );
}
- foreach ($a as $row) {
- $this->insertOneRow($table, $row, $fname);
+
+ foreach ( $a as &$row ) {
+ $this->insertOneRow( $table, $row, $fname );
}
- //$this->ignoreErrors($oldIgnore);
$retVal = true;
- //if (in_array('IGNORE', $options))
- // $this->ignoreErrors($oldIgnore);
+ if ( in_array( 'IGNORE', $options ) ) {
+ $this->ignore_DUP_VAL_ON_INDEX = false;
+ }
return $retVal;
}
- function insertOneRow($table, $row, $fname) {
+ private function insertOneRow( $table, $row, $fname ) {
+ global $wgLang;
+
+ $table = $this->tableName( $table );
// "INSERT INTO tables (a, b, c)"
- $sql = "INSERT INTO " . $this->tableName($table) . " (" . join(',', array_keys($row)) . ')';
+ $sql = "INSERT INTO " . $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";
+ foreach ( $row as $col => $val ) {
+ if ( $first ) {
+ $sql .= $val !== null ? ':' . $col : 'NULL';
+ } else {
+ $sql .= $val !== null ? ', :' . $col : ', NULL';
+ }
+
$first = false;
}
- $sql .= ") $returning";
+ $sql .= ')';
- $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__);
+ $stmt = oci_parse( $this->mConn, $sql );
+ foreach ( $row as $col => &$val ) {
+ $col_info = $this->fieldInfoMulti( $table, $col );
+ $col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
+
+ if ( $val === null ) {
+ // do nothing ... null was inserted in statement creation
+ } elseif ( $col_type != 'BLOB' && $col_type != 'CLOB' ) {
+ if ( is_object( $val ) ) {
+ $val = $val->getData();
+ }
+
+ if ( preg_match( '/^timestamp.*/i', $col_type ) == 1 && strtolower( $val ) == 'infinity' ) {
+ $val = '31-12-2030 12:00:00.000000';
+ }
+
+ $val = ( $wgLang != null ) ? $wgLang->checkTitleEncoding( $val ) : $val;
+ if ( oci_bind_by_name( $stmt, ":$col", $val ) === false ) {
+ $this->reportQueryError( $this->lastErrno(), $this->lastError(), $sql, __METHOD__ );
+ return false;
+ }
+ } else {
+ if ( ( $lob[$col] = oci_new_descriptor( $this->mConn, OCI_D_LOB ) ) === false ) {
+ $e = oci_error( $stmt );
+ throw new DBUnexpectedError( $this, "Cannot create LOB descriptor: " . $e['message'] );
+ }
+
+ if ( $col_type == 'BLOB' ) { // is_object($val)) {
+ $lob[$col]->writeTemporary( $val ); // ->getData());
+ oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, SQLT_BLOB );
+ } else {
+ $lob[$col]->writeTemporary( $val );
+ oci_bind_by_name( $stmt, ":$col", $lob[$col], - 1, OCI_B_CLOB );
+ }
}
}
- 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']);
+ wfSuppressWarnings();
+
+ if ( oci_execute( $stmt, OCI_DEFAULT ) === false ) {
+ $e = oci_error( $stmt );
+
+ if ( !$this->ignore_DUP_VAL_ON_INDEX || $e['code'] != '1' ) {
+ $this->reportQueryError( $e['message'], $e['code'], $sql, __METHOD__ );
+ return false;
+ } else {
+ $this->mAffectedRows = oci_num_rows( $stmt );
+ }
+ } else {
+ $this->mAffectedRows = oci_num_rows( $stmt );
+ }
+
+ wfRestoreWarnings();
+
+ if ( isset( $lob ) ) {
+ foreach ( $lob as $lob_i => $lob_v ) {
+ $lob_v->free();
+ }
+ }
+
+ if ( !$this->mTrxLevel ) {
+ oci_commit( $this->mConn );
+ }
+
+ oci_free_statement( $stmt );
+ }
+
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabaseOracle::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+ 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 );
}
- if (strlen($returning))
- oci_bind_by_name($stmt, ":bval", $bval, -1, SQLT_BLOB);
+ if ( ( $sequenceData = $this->getSequenceData( $destTable ) ) !== false &&
+ !isset( $varMap[$sequenceData['column']] ) )
+ $varMap[$sequenceData['column']] = 'GET_SEQUENCE_VALUE(\'' . $sequenceData['sequence'] . '\')';
- if (oci_execute($stmt, OCI_DEFAULT) === false) {
- $e = oci_error($stmt);
- $this->reportQueryError($e['message'], $e['code'], $sql, __METHOD__);
+ // count-alias subselect fields to avoid abigious definition errors
+ $i = 0;
+ foreach ( $varMap as $key => &$val ) {
+ $val = $val . ' field' . ( $i++ );
}
- if (strlen($returning)) {
- $bval->save($row[$blobcol]->getData());
- $bval->free();
+
+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex ";
+ if ( $conds != '*' ) {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+ $sql .= " $tailOpts";
+
+ if ( in_array( 'IGNORE', $insertOptions ) ) {
+ $this->ignore_DUP_VAL_ON_INDEX = true;
}
- if (!$this->mTrxLevel)
- oci_commit($this->mConn);
- oci_free_statement($stmt);
+ $retval = $this->query( $sql, $fname );
+
+ if ( in_array( 'IGNORE', $insertOptions ) ) {
+ $this->ignore_DUP_VAL_ON_INDEX = false;
+ }
+
+ return $retval;
}
function tableName( $name ) {
- # Replace reserved words with better ones
+ global $wgSharedDB, $wgSharedPrefix, $wgSharedTables;
+ /*
+ Replace reserved words with better ones
+ Using uppercase because that's the only way Oracle can handle
+ quoted tablenames
+ */
switch( $name ) {
case 'user':
- return 'mwuser';
+ $name = 'MWUSER';
+ break;
case 'text':
- return 'pagecontent';
- default:
- return $name;
+ $name = 'PAGECONTENT';
+ break;
+ }
+
+ /*
+ The rest of procedure is equal to generic Databse class
+ except for the quoting style
+ */
+ if ( $name[0] == '"' && substr( $name, - 1, 1 ) == '"' ) {
+ return $name;
+ }
+ if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
+ return $name;
+ }
+ $dbDetails = array_reverse( explode( '.', $name, 2 ) );
+ if ( isset( $dbDetails[1] ) ) {
+ @list( $table, $database ) = $dbDetails;
+ } else {
+ @list( $table ) = $dbDetails;
+ }
+
+ $prefix = $this->mTablePrefix;
+
+ if ( isset( $database ) ) {
+ $table = ( $table[0] == '`' ? $table : "`{$table}`" );
}
+
+ if ( !isset( $database ) && isset( $wgSharedDB ) && $table[0] != '"'
+ && isset( $wgSharedTables )
+ && is_array( $wgSharedTables )
+ && in_array( $table, $wgSharedTables )
+ ) {
+ $database = $wgSharedDB;
+ $prefix = isset( $wgSharedPrefix ) ? $wgSharedPrefix : $prefix;
+ }
+
+ if ( isset( $database ) ) {
+ $database = ( $database[0] == '"' ? $database : "\"{$database}\"" );
+ }
+ $table = ( $table[0] == '"' ? $table : "\"{$prefix}{$table}\"" );
+
+ $tableName = ( isset( $database ) ? "{$database}.{$table}" : "{$table}" );
+
+ return strtoupper( $tableName );
}
/**
* 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);
+ function nextSequenceValue( $seqName ) {
+ $res = $this->query( "SELECT $seqName.nextval FROM dual" );
+ $row = $this->fetchRow( $res );
$this->mInsertId = $row[0];
- $this->freeResult($res);
+ $this->freeResult( $res );
return $this->mInsertId;
}
/**
- * Oracle does not have a "USE INDEX" clause, so return an empty string
+ * Return sequence_name if table has a sequence
*/
- function useIndexClause($index) {
- return '';
+ private function getSequenceData( $table ) {
+ if ( $this->sequenceData == null ) {
+ $result = $this->query( "SELECT lower(us.sequence_name), lower(utc.table_name), lower(utc.column_name) from user_sequences us, user_tab_columns utc where us.sequence_name = utc.table_name||'_'||utc.column_name||'_SEQ'" );
+
+ while ( ( $row = $result->fetchRow() ) !== false ) {
+ $this->sequenceData[$this->tableName( $row[1] )] = array(
+ 'sequence' => $row[0],
+ 'column' => $row[2]
+ );
+ }
+ }
+
+ return ( isset( $this->sequenceData[$table] ) ) ? $this->sequenceData[$table] : false;
}
# REPLACE query wrapper
@@ -411,59 +685,44 @@ class DatabaseOracle extends Database {
# 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);
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseOracle::replace' ) {
+ $table = $this->tableName( $table );
- if (count($rows)==0) {
+ if ( count( $rows ) == 0 ) {
return;
}
# Single row case
- if (!is_array(reset($rows))) {
- $rows = array($rows);
+ if ( !is_array( reset( $rows ) ) ) {
+ $rows = array( $rows );
}
- foreach( $rows as $row ) {
+ $sequenceData = $this->getSequenceData( $table );
+
+ 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] );
- }
+ $condsDelete = array();
+ foreach ( $uniqueIndexes as $index )
+ $condsDelete[$index] = $row[$index];
+ if (count($condsDelete) > 0) {
+ $this->delete( $table, $condsDelete, $fname );
}
- $sql .= ')';
- $this->query( $sql, $fname );
+ }
+
+ if ( $sequenceData !== false && !isset( $row[$sequenceData['column']] ) ) {
+ $row[$sequenceData['column']] = $this->nextSequenceValue( $sequenceData['sequence'] );
}
# 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);
+ $this->insert( $table, $row, $fname );
}
}
# DELETE where the condition is a join
- function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "Database::deleteJoin" ) {
+ function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = "DatabaseOracle::deleteJoin" ) {
if ( !$conds ) {
- throw new DBUnexpectedError($this, 'Database::deleteJoin() called with empty $conds' );
+ throw new DBUnexpectedError( $this, 'DatabaseOracle::deleteJoin() called with empty $conds' );
}
$delTable = $this->tableName( $delTable );
@@ -479,78 +738,80 @@ class DatabaseOracle extends Database {
# 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;
+ $fieldInfoData = $this->fieldInfo( $table, $field);
+ if ( $fieldInfoData->type == "varchar" ) {
+ $size = $row->size - 4;
} else {
- $size=$row->size;
+ $size = $row->size;
}
- $this->freeResult( $res );
return $size;
}
- function lowPriorityOption() {
- return '';
- }
-
- function limitResult($sql, $limit, $offset) {
- if ($offset === false)
+ function limitResult( $sql, $limit, $offset = false ) {
+ if ( $offset === false ) {
$offset = 0;
- return "SELECT * FROM ($sql) WHERE rownum >= (1 + $offset) AND rownum < 1 + $limit + $offset";
+ }
+ 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 $cond String: SQL expression which will result in a boolean value
- * @param $trueVal String: SQL expression to return if true
- * @param $falseVal String: SQL expression to return if false
- * @return String: SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
+
+ function unionQueries( $sqls, $all ) {
+ $glue = ' UNION ALL ';
+ return 'SELECT * ' . ( $all ? '':'/* UNION_UNIQUE */ ' ) . 'FROM (' . implode( $glue, $sqls ) . ')' ;
}
function wasDeadlock() {
return $this->lastErrno() == 'OCI-00060';
}
- function timestamp($ts = 0) {
- return wfTimestamp(TS_ORACLE, $ts);
+
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseOracle::duplicateTableStructure' ) {
+ $temporary = $temporary ? 'TRUE' : 'FALSE';
+ $oldName = trim(strtoupper($oldName), '"');
+ $oldParts = explode('_', $oldName);
+
+ $newName = trim(strtoupper($newName), '"');
+ $newParts = explode('_', $newName);
+
+ $oldPrefix = '';
+ $newPrefix = '';
+ for ($i = count($oldParts)-1; $i >= 0; $i--) {
+ if ($oldParts[$i] != $newParts[$i]) {
+ $oldPrefix = implode('_', $oldParts).'_';
+ $newPrefix = implode('_', $newParts).'_';
+ break;
+ }
+ unset($oldParts[$i]);
+ unset($newParts[$i]);
+ }
+
+ $tabName = substr($oldName, strlen($oldPrefix));
+
+ return $this->query( 'BEGIN DUPLICATE_TABLE(\'' . $tabName . '\', \'' . $oldPrefix . '\', \''.$newPrefix.'\', ' . $temporary . '); END;', $fname );
+ }
+
+ function timestamp( $ts = 0 ) {
+ return wfTimestamp( TS_ORACLE, $ts );
}
/**
* Return aggregated value function call
*/
- function aggregateValue ($valuedata,$valuename='value') {
+ function aggregateValue ( $valuedata, $valuename = 'value' ) {
return $valuedata;
}
- function reportQueryError($error, $errno, $sql, $fname, $tempIgnore = false) {
+ function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
# Ignore errors during error handling to avoid infinite
# recursion
- $ignore = $this->ignoreErrors(true);
+ $ignore = $this->ignoreErrors( true );
++$this->mErrorCount;
- if ($ignore || $tempIgnore) {
-echo "error ignored! query = [$sql]\n";
- wfDebug("SQL ERROR (ignored): $error\n");
+ if ( $ignore || $tempIgnore ) {
+ 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);
+ } else {
+ throw new DBQueryError( $this, $error, $errno, $sql, $fname );
}
}
@@ -558,80 +819,252 @@ echo "error!\n";
* @return string wikitext of a link to the server software's web site
*/
function getSoftwareLink() {
- return "[http://www.oracle.com/ Oracle]";
+ return '[http://www.oracle.com/ Oracle]';
}
/**
* @return string Version information from the database
*/
function getServerVersion() {
- return oci_server_version($this->mConn);
+ 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);
+ function tableExists( $table ) {
+ $SQL = "SELECT 1 FROM user_tables WHERE table_name='$table'";
+ $res = $this->doQuery( $SQL );
+ if ( $res ) {
+ $count = $res->numRows();
+ $res->free();
+ } else {
+ $count = 0;
+ }
return $count;
}
/**
- * Query whether a given column exists in the mediawiki schema
+ * Function translates mysql_fetch_field() functionality on ORACLE.
+ * Caching is present for reducing query time.
+ * For internal calls. Use fieldInfo for normal usage.
+ * Returns false if the field doesn't exist
+ *
+ * @param Array $table
+ * @param String $field
*/
- function fieldExists( $table, $field ) {
- return true; // XXX
+ private function fieldInfoMulti( $table, $field ) {
+ $tableWhere = '';
+ $field = strtoupper($field);
+ if (is_array($table)) {
+ $table = array_map( array( &$this, 'tableName' ), $table );
+ $tableWhere = 'IN (';
+ foreach($table as &$singleTable) {
+ $singleTable = strtoupper(trim( $singleTable, '"' ));
+ if (isset($this->mFieldInfoCache["$singleTable.$field"])) {
+ return $this->mFieldInfoCache["$singleTable.$field"];
+ }
+ $tableWhere .= '\''.$singleTable.'\',';
+ }
+ $tableWhere = rtrim($tableWhere, ',').')';
+ } else {
+ $table = strtoupper(trim( $this->tableName($table), '"' ));
+ if (isset($this->mFieldInfoCache["$table.$field"])) {
+ return $this->mFieldInfoCache["$table.$field"];
+ }
+ $tableWhere = '= \''.$table.'\'';
+ }
+
+ $fieldInfoStmt = oci_parse( $this->mConn, 'SELECT * FROM wiki_field_info_full WHERE table_name '.$tableWhere.' and column_name = \''.$field.'\'' );
+ if ( oci_execute( $fieldInfoStmt, OCI_DEFAULT ) === false ) {
+ $e = oci_error( $fieldInfoStmt );
+ $this->reportQueryError( $e['message'], $e['code'], 'fieldInfo QUERY', __METHOD__ );
+ return false;
+ }
+ $res = new ORAResult( $this, $fieldInfoStmt );
+ if ($res->numRows() == 0 ) {
+ if (is_array($table)) {
+ foreach($table as &$singleTable) {
+ $this->mFieldInfoCache["$singleTable.$field"] = false;
+ }
+ } else {
+ $this->mFieldInfoCache["$table.$field"] = false;
+ }
+ } else {
+ $fieldInfoTemp = new ORAField( $res->fetchRow() );
+ $table = $fieldInfoTemp->tableName();
+ $this->mFieldInfoCache["$table.$field"] = $fieldInfoTemp;
+ return $fieldInfoTemp;
+ }
}
function fieldInfo( $table, $field ) {
- return false; // XXX
+ if ( is_array( $table ) ) {
+ throw new DBUnexpectedError( $this, 'Database::fieldInfo called with table array!' );
+ }
+ return $this->fieldInfoMulti ($table, $field);
+ }
+
+ function fieldExists( $table, $field, $fname = 'DatabaseOracle::fieldExists' ) {
+ return (bool)$this->fieldInfo( $table, $field, $fname );
}
function begin( $fname = '' ) {
$this->mTrxLevel = 1;
}
+
function immediateCommit( $fname = '' ) {
return true;
}
+
function commit( $fname = '' ) {
- oci_commit($this->mConn);
+ oci_commit( $this->mConn );
$this->mTrxLevel = 0;
}
/* Not even sure why this is used in the main codebase... */
- function limitResultForUpdate($sql, $num) {
+ function limitResultForUpdate( $sql, $num ) {
return $sql;
}
- function strencode($s) {
- return str_replace("'", "''", $s);
+ /* defines must comply with ^define\s*([^\s=]*)\s*=\s?'\{\$([^\}]*)\}'; */
+ function sourceStream( $fp, $lineCallback = false, $resultCallback = false ) {
+ $cmd = '';
+ $done = false;
+ $dollarquote = false;
+
+ $replacements = array();
+
+ 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, 8 ) == '/*$mw$*/' ) {
+ if ( $dollarquote ) {
+ $dollarquote = false;
+ $done = true;
+ } else {
+ $dollarquote = true;
+ }
+ } elseif ( !$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 );
+ if ( strtolower( substr( $cmd, 0, 6 ) ) == 'define' ) {
+ if ( preg_match( '/^define\s*([^\s=]*)\s*=\s*\'\{\$([^\}]*)\}\'/', $cmd, $defines ) ) {
+ $replacements[$defines[2]] = $defines[1];
+ }
+ } else {
+ foreach ( $replacements as $mwVar => $scVar ) {
+ $cmd = str_replace( '&' . $scVar . '.', '{$' . $mwVar . '}', $cmd );
+ }
+
+ $cmd = $this->replaceVars( $cmd );
+ $res = $this->query( $cmd, __METHOD__ );
+ if ( $resultCallback ) {
+ call_user_func( $resultCallback, $res, $this );
+ }
+
+ if ( false === $res ) {
+ $err = $this->lastError();
+ return "Query \"{$cmd}\" failed with error code \"$err\".\n";
+ }
+ }
+
+ $cmd = '';
+ $done = false;
+ }
+ }
+ return true;
}
- function encodeBlob($b) {
- return new ORABlob($b);
+ function setup_database() {
+ global $wgVersion, $wgDBmwschema, $wgDBts2schema, $wgDBport, $wgDBuser;
+
+ $res = $this->sourceFile( "../maintenance/ora/tables.sql" );
+ if ($res === true) {
+ print " done.</li>\n";
+ } else {
+ print " <b>FAILED</b></li>\n";
+ dieout( htmlspecialchars( $res ) );
+ }
+
+ // Avoid the non-standard "REPLACE INTO" syntax
+ echo "<li>Populating interwiki table</li>\n";
+ $f = fopen( "../maintenance/interwiki.sql", 'r' );
+ if ( $f == false ) {
+ dieout( "Could not find the interwiki.sql file" );
+ }
+
+ // do it like the postgres :D
+ $SQL = "INSERT INTO ".$this->tableName('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])" );
+ }
+
+ echo "<li>Table interwiki successfully populated</li>\n";
}
- function decodeBlob($b) {
- return $b; //return $b->load();
+
+ function strencode( $s ) {
+ return str_replace( "'", "''", $s );
}
function addQuotes( $s ) {
- global $wgLang;
- $s = $wgLang->checkTitleEncoding($s);
- return "'" . $this->strencode($s) . "'";
+ global $wgLang;
+ if ( isset( $wgLang->mLoaded ) && $wgLang->mLoaded ) {
+ $s = $wgLang->checkTitleEncoding( $s );
+ }
+ return "'" . $this->strencode( $s ) . "'";
}
function quote_ident( $s ) {
return $s;
}
- /* For now, does nothing */
- function selectDB( $db ) {
- return true;
+ function selectRow( $table, $vars, $conds, $fname = 'DatabaseOracle::selectRow', $options = array(), $join_conds = array() ) {
+ global $wgLang;
+
+ $conds2 = array();
+ $conds = ($conds != null && !is_array($conds)) ? array($conds) : $conds;
+ foreach ( $conds as $col => $val ) {
+ $col_info = $this->fieldInfoMulti( $table, $col );
+ $col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
+ if ( $col_type == 'CLOB' ) {
+ $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val );
+ } elseif ( $col_type == 'VARCHAR2' && !mb_check_encoding( $val ) ) {
+ $conds2[$col] = $wgLang->checkTitleEncoding( $val );
+ } else {
+ $conds2[$col] = $val;
+ }
+ }
+
+ return parent::selectRow( $table, $vars, $conds2, $fname, $options, $join_conds );
}
/**
@@ -655,18 +1088,18 @@ echo "error!\n";
}
}
- 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( $options['GROUP BY'] ) ) {
+ $preLimitTail .= " GROUP BY {$options['GROUP BY']}";
+ }
+ if ( isset( $options['ORDER BY'] ) ) {
+ $preLimitTail .= " ORDER BY {$options['ORDER BY']}";
}
- #if ( isset( $noKeyOptions['FOR UPDATE'] ) ) $tailOpts .= ' FOR UPDATE';
- #if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) $tailOpts .= ' LOCK IN SHARE MODE';
- if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) $startOpts .= 'DISTINCT';
+ # if ( isset( $noKeyOptions['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'] );
@@ -677,13 +1110,46 @@ echo "error!\n";
return array( $startOpts, $useIndex, $preLimitTail, $postLimitTail );
}
- public function setTimeout( $timeout ) {
- // @todo fixme no-op
+ public function delete( $table, $conds, $fname = 'DatabaseOracle::delete' ) {
+ global $wgLang;
+
+ if ( $wgLang != null ) {
+ $conds2 = array();
+ $conds = ($conds != null && !is_array($conds)) ? array($conds) : $conds;
+ foreach ( $conds as $col => $val ) {
+ $col_info = $this->fieldInfoMulti( $table, $col );
+ $col_type = $col_info != false ? $col_info->type() : 'CONSTANT';
+ if ( $col_type == 'CLOB' ) {
+ $conds2['TO_CHAR(' . $col . ')'] = $wgLang->checkTitleEncoding( $val );
+ } else {
+ if ( is_array( $val ) ) {
+ $conds2[$col] = $val;
+ foreach ( $conds2[$col] as &$val2 ) {
+ $val2 = $wgLang->checkTitleEncoding( $val2 );
+ }
+ } else {
+ $conds2[$col] = $wgLang->checkTitleEncoding( $val );
+ }
+ }
+ }
+
+ return parent::delete( $table, $conds2, $fname );
+ } else {
+ return parent::delete( $table, $conds, $fname );
+ }
}
- function ping() {
- wfDebug( "Function ping() not written for DatabaseOracle.php yet");
- return true;
+ function bitNot( $field ) {
+ // expecting bit-fields smaller than 4bytes
+ return 'BITNOT(' . $bitField . ')';
+ }
+
+ function bitAnd( $fieldLeft, $fieldRight ) {
+ return 'BITAND(' . $fieldLeft . ', ' . $fieldRight . ')';
+ }
+
+ function bitOr( $fieldLeft, $fieldRight ) {
+ return 'BITOR(' . $fieldLeft . ', ' . $fieldRight . ')';
}
/**
@@ -696,8 +1162,8 @@ echo "error!\n";
return 0;
}
- function setFakeSlaveLag( $lag ) {}
- function setFakeMaster( $enabled = true ) {}
+ function setFakeSlaveLag( $lag ) { }
+ function setFakeMaster( $enabled = true ) { }
function getDBname() {
return $this->mDBname;
@@ -706,19 +1172,28 @@ echo "error!\n";
function getServer() {
return $this->mServer;
}
-
- /**
- * No-op lock functions
- */
- public function lock( $lockName, $method ) {
- return true;
- }
- public function unlock( $lockName, $method ) {
- return true;
+
+ public function replaceVars( $ins ) {
+ $varnames = array( 'wgDBprefix' );
+ if ( $this->mFlags & DBO_SYSDBA ) {
+ $varnames[] = 'wgDBOracleDefTS';
+ $varnames[] = 'wgDBOracleTempTS';
+ }
+
+ // 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 );
+ }
+ }
+
+ return parent::replaceVars( $ins );
}
-
+
public function getSearchEngine() {
- return "SearchOracle";
+ return 'SearchOracle';
}
-
} // end DatabaseOracle class
diff --git a/includes/db/DatabasePostgres.php b/includes/db/DatabasePostgres.php
index c940ad09..9072a5b2 100644
--- a/includes/db/DatabasePostgres.php
+++ b/includes/db/DatabasePostgres.php
@@ -11,7 +11,7 @@ class PostgresField {
static function fromText($db, $table, $field) {
global $wgDBmwschema;
- $q = <<<END
+ $q = <<<SQL
SELECT
CASE WHEN typname = 'int2' THEN 'smallint'
WHEN typname = 'int4' THEN 'integer'
@@ -27,7 +27,7 @@ AND atttypid=pg_type.oid
AND nspname=%s
AND relname=%s
AND attname=%s;
-END;
+SQL;
$res = $db->query(sprintf($q,
$db->addQuotes($wgDBmwschema),
$db->addQuotes($table),
@@ -68,11 +68,11 @@ END;
/**
* @ingroup Database
*/
-class DatabasePostgres extends Database {
- var $mInsertId = NULL;
- var $mLastResult = NULL;
- var $numeric_version = NULL;
- var $mAffectedRows = NULL;
+class DatabasePostgres extends DatabaseBase {
+ var $mInsertId = null;
+ var $mLastResult = null;
+ var $numeric_version = null;
+ var $mAffectedRows = null;
function DatabasePostgres($server = false, $user = false, $password = false, $dbName = false,
$failFunction = false, $flags = 0 )
@@ -84,6 +84,10 @@ class DatabasePostgres extends Database {
}
+ function getType() {
+ return 'postgres';
+ }
+
function cascadingDeletes() {
return true;
}
@@ -132,8 +136,8 @@ class DatabasePostgres extends Database {
global $wgDBport;
- if (!strlen($user)) { ## e.g. the class is being loaded
- return;
+ if (!strlen($user)) { ## e.g. the class is being loaded
+ return;
}
$this->close();
$this->mServer = $server;
@@ -152,7 +156,7 @@ class DatabasePostgres extends Database {
if ($port!=false && $port!="") {
$connectVars['port'] = $port;
}
- $connectString = $this->makeConnectionString( $connectVars );
+ $connectString = $this->makeConnectionString( $connectVars, PGSQL_CONNECT_FORCE_NEW );
$this->installErrorHandler();
$this->mConn = pg_connect( $connectString );
@@ -578,7 +582,7 @@ class DatabasePostgres extends Database {
$sql = mb_convert_encoding($sql,'UTF-8');
}
$this->mLastResult = pg_query( $this->mConn, $sql);
- $this->mAffectedRows = NULL; // use pg_affected_rows(mLastResult)
+ $this->mAffectedRows = null; // use pg_affected_rows(mLastResult)
return $this->mLastResult;
}
@@ -713,7 +717,7 @@ class DatabasePostgres extends Database {
$sql = "SELECT indexname FROM pg_indexes WHERE tablename='$table'";
$res = $this->query( $sql, $fname );
if ( !$res ) {
- return NULL;
+ return null;
}
while ( $row = $this->fetchObject( $res ) ) {
if ( $row->indexname == $this->indexName( $index ) ) {
@@ -730,7 +734,7 @@ class DatabasePostgres extends Database {
")'";
$res = $this->query( $sql, $fname );
if ( !$res )
- return NULL;
+ return null;
while ($row = $this->fetchObject( $res ))
return true;
return false;
@@ -873,6 +877,81 @@ class DatabasePostgres extends Database {
}
+ /**
+ * INSERT SELECT wrapper
+ * $varMap must be an associative array of the form array( 'dest1' => 'source1', ...)
+ * Source items may be literals rather then 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.
+ * @todo FIXME: implement this a little better (seperate select/insert)?
+ */
+ function insertSelect( $destTable, $srcTable, $varMap, $conds, $fname = 'DatabasePostgres::insertSelect',
+ $insertOptions = array(), $selectOptions = array() )
+ {
+ $destTable = $this->tableName( $destTable );
+
+ // If IGNORE is set, we use savepoints to emulate mysql's behavior
+ $ignore = in_array( 'IGNORE', $insertOptions ) ? 'mw' : '';
+
+ 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 );
+ }
+
+ // 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 );
+ $numrowsinserted = 0;
+ pg_query( $this->mConn, "SAVEPOINT $ignore");
+ }
+
+ $sql = "INSERT INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
+ " SELECT $startOpts " . implode( ',', $varMap ) .
+ " FROM $srcTable $useIndex";
+
+ if ( $conds != '*') {
+ $sql .= ' WHERE ' . $this->makeList( $conds, LIST_AND );
+ }
+
+ $sql .= " $tailOpts";
+
+ $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++;
+ }
+ $olde = error_reporting( $olde );
+ if( $didbegin ) {
+ $this->commit();
+ }
+
+ // Set the affected row count for the whole operation
+ $this->mAffectedRows = $numrowsinserted;
+
+ // IGNORE always returns true
+ return true;
+ }
+
+ return $res;
+ }
+
function tableName( $name ) {
# Replace reserved words with better ones
switch( $name ) {
@@ -898,7 +977,7 @@ class DatabasePostgres extends Database {
}
/**
- * Return the current value of a sequence. Assumes it has ben nextval'ed in this session.
+ * Return the current value of a sequence. Assumes it has been nextval'ed in this session.
*/
function currentSequenceValue( $seqName ) {
$safeseq = preg_replace( "/'/", "''", $seqName );
@@ -909,13 +988,6 @@ class DatabasePostgres extends Database {
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
@@ -1009,31 +1081,18 @@ class DatabasePostgres extends Database {
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 $cond String: SQL expression which will result in a boolean value
- * @param $trueVal String: SQL expression to return if true
- * @param $falseVal String: SQL expression to return if false
- * @return String: SQL fragment
- */
- function conditional( $cond, $trueVal, $falseVal ) {
- return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
- }
-
function wasDeadlock() {
return $this->lastErrno() == '40P01';
}
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabasePostgres::duplicateTableStructure' ) {
+ return $this->query( 'CREATE ' . ( $temporary ? 'TEMPORARY ' : '' ) . " TABLE $newName (LIKE $oldName INCLUDING DEFAULTS)", $fname );
+ }
+
function timestamp( $ts=0 ) {
return wfTimestamp(TS_POSTGRES,$ts);
}
@@ -1126,18 +1185,18 @@ class DatabasePostgres extends Database {
function triggerExists( $table, $trigger ) {
global $wgDBmwschema;
- $q = <<<END
+ $q = <<<SQL
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;
+SQL;
$res = $this->query(sprintf($q,
$this->addQuotes($wgDBmwschema),
$this->addQuotes($table),
$this->addQuotes($trigger)));
if (!$res)
- return NULL;
+ return null;
$rows = $res->numRows();
$this->freeResult( $res );
return $rows;
@@ -1161,7 +1220,7 @@ END;
$this->addQuotes($constraint));
$res = $this->query($SQL);
if (!$res)
- return NULL;
+ return null;
$rows = $res->numRows();
$this->freeResult($res);
return $rows;
@@ -1252,11 +1311,17 @@ END;
if (!$res) {
print "<b>FAILED</b>. Make sure that the user \"" . htmlspecialchars( $wgDBuser ) .
"\" can write to the schema \"" . htmlspecialchars( $wgDBmwschema ) . "\"</li>\n";
- dieout("</ul>");
+ dieout(""); # Will close the main list <ul> and finish the page.
}
$this->doQuery("DROP TABLE $safeschema.$ctest");
- $res = dbsource( "../maintenance/postgres/tables.sql", $this);
+ $res = $this->sourceFile( "../maintenance/postgres/tables.sql" );
+ if ($res === true) {
+ print " done.</li>\n";
+ } else {
+ print " <b>FAILED</b></li>\n";
+ dieout( htmlspecialchars( $res ) );
+ }
## Update version information
$mwv = $this->addQuotes($wgVersion);
@@ -1274,10 +1339,13 @@ END;
"WHERE type = 'Creation'";
$this->query($SQL);
+ echo "<li>Populating interwiki table... ";
+
## 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");
+ print "<b>FAILED</b></li>";
+ dieout( "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 ";
@@ -1289,7 +1357,7 @@ END;
}
$this->query("$SQL $matches[1],$matches[2])");
}
- print " (table interwiki successfully populated)...\n";
+ print " successfully populated.</li>\n";
$this->doQuery("COMMIT");
}
@@ -1324,11 +1392,6 @@ END;
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
@@ -1392,15 +1455,6 @@ END;
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?
*
@@ -1425,17 +1479,7 @@ END;
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;
- }
-
public function getSearchEngine() {
return "SearchPostgres";
}
-
} // end DatabasePostgres class
diff --git a/includes/db/DatabaseSqlite.php b/includes/db/DatabaseSqlite.php
index 455c0b48..c149cf04 100644
--- a/includes/db/DatabaseSqlite.php
+++ b/includes/db/DatabaseSqlite.php
@@ -10,7 +10,7 @@
/**
* @ingroup Database
*/
-class DatabaseSqlite extends Database {
+class DatabaseSqlite extends DatabaseBase {
var $mAffectedRows;
var $mLastResult;
@@ -18,110 +18,162 @@ class DatabaseSqlite extends Database {
var $mName;
/**
- * Constructor
+ * Constructor.
+ * Parameters $server, $user and $password are not used.
*/
- function __construct($server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0) {
- global $wgOut,$wgSQLiteDataDir, $wgSQLiteDataDirMode;
- if ("$wgSQLiteDataDir" == '') $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data';
- if (!is_dir($wgSQLiteDataDir)) wfMkdirParents( $wgSQLiteDataDir, $wgSQLiteDataDirMode );
+ function __construct( $server = false, $user = false, $password = false, $dbName = false, $failFunction = false, $flags = 0 ) {
$this->mFailFunction = $failFunction;
$this->mFlags = $flags;
- $this->mDatabaseFile = "$wgSQLiteDataDir/$dbName.sqlite";
$this->mName = $dbName;
- $this->open($server, $user, $password, $dbName);
+ $this->open( $server, $user, $password, $dbName );
+ }
+
+ function getType() {
+ return 'sqlite';
}
/**
- * todo: check if these should be true like parent class
+ * @todo: check if it 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);
+ 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;
- try {
- if ( $this->mFlags & DBO_PERSISTENT ) {
- $this->mConn = new PDO( "sqlite:$file", $user, $pass,
- array( PDO::ATTR_PERSISTENT => true ) );
- } else {
- $this->mConn = new PDO( "sqlite:$file", $user, $pass );
- }
- } catch ( PDOException $e ) {
- $err = $e->getMessage();
- }
- if ( $this->mConn === false ) {
- wfDebug( "DB connection error: $err\n" );
- if ( !$this->mFailFunction ) {
- throw new DBConnectionError( $this, $err );
- } else {
- return false;
- }
+ function open( $server, $user, $pass, $dbName ) {
+ global $wgSQLiteDataDir;
- }
- $this->mOpened = $this->mConn;
- # set error codes only, don't raise exceptions
- $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+ $fileName = self::generateFileName( $wgSQLiteDataDir, $dbName );
+ if ( !is_readable( $fileName ) ) {
+ throw new DBConnectionError( $this, "SQLite database not accessible" ); $this->mConn = false;
}
+ $this->openFile( $fileName );
return $this->mConn;
}
/**
+ * Opens a database file
+ * @return SQL connection or false if failed
+ */
+ function openFile( $fileName ) {
+ $this->mDatabaseFile = $fileName;
+ try {
+ if ( $this->mFlags & DBO_PERSISTENT ) {
+ $this->mConn = new PDO( "sqlite:$fileName", '', '',
+ array( PDO::ATTR_PERSISTENT => true ) );
+ } else {
+ $this->mConn = new PDO( "sqlite:$fileName", '', '' );
+ }
+ } catch ( PDOException $e ) {
+ $err = $e->getMessage();
+ }
+ if ( $this->mConn === false ) {
+ wfDebug( "DB connection error: $err\n" );
+ if ( !$this->mFailFunction ) {
+ throw new DBConnectionError( $this, $err );
+ } else {
+ return false;
+ }
+
+ }
+ $this->mOpened = !!$this->mConn;
+ # set error codes only, don't raise exceptions
+ if ( $this->mOpened ) {
+ $this->mConn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
+ return true;
+ }
+ }
+
+ /**
* Close an SQLite database
*/
function close() {
$this->mOpened = false;
- if (is_object($this->mConn)) {
- if ($this->trxLevel()) $this->immediateCommit();
+ if ( is_object( $this->mConn ) ) {
+ if ( $this->trxLevel() ) $this->commit();
$this->mConn = null;
}
return true;
}
/**
+ * Generates a database file name. Explicitly public for installer.
+ * @param $dir String: Directory where database resides
+ * @param $dbName String: Database name
+ * @return String
+ */
+ public static function generateFileName( $dir, $dbName ) {
+ return "$dir/$dbName.sqlite";
+ }
+
+ /**
+ * Returns version of currently supported SQLite fulltext search module or false if none present.
+ * @return String
+ */
+ function getFulltextSearchModule() {
+ $table = 'dummy_search_test';
+ $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ );
+ if ( $this->query( "CREATE VIRTUAL TABLE $table USING FTS3(dummy_field)", __METHOD__, true ) ) {
+ $this->query( "DROP TABLE IF EXISTS $table", __METHOD__ );
+ return 'FTS3';
+ }
+ return false;
+ }
+
+ /**
* 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) {
+ function doQuery( $sql ) {
+ $res = $this->mConn->query( $sql );
+ if ( $res === false ) {
return false;
} else {
$r = $res instanceof ResultWrapper ? $res->result : $res;
$this->mAffectedRows = $r->rowCount();
- $res = new ResultWrapper($this,$r->fetchAll());
+ $res = new ResultWrapper( $this, $r->fetchAll() );
}
return $res;
}
- function freeResult($res) {
- if ($res instanceof ResultWrapper) $res->result = NULL; else $res = NULL;
+ 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);
+ 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;
+ 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);
+ 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;
@@ -130,20 +182,20 @@ class DatabaseSqlite extends Database {
/**
* The PDO::Statement class implements the array interface so count() will work
*/
- function numRows($res) {
+ function numRows( $res ) {
$r = $res instanceof ResultWrapper ? $res->result : $res;
- return count($r);
+ return count( $r );
}
- function numFields($res) {
+ function numFields( $res ) {
$r = $res instanceof ResultWrapper ? $res->result : $res;
- return is_array($r) ? count($r[0]) : 0;
+ return is_array( $r ) ? count( $r[0] ) : 0;
}
- function fieldName($res,$n) {
+ function fieldName( $res, $n ) {
$r = $res instanceof ResultWrapper ? $res->result : $res;
- if (is_array($r)) {
- $keys = array_keys($r[0]);
+ if ( is_array( $r ) ) {
+ $keys = array_keys( $r[0] );
return $keys[$n];
}
return false;
@@ -152,8 +204,8 @@ class DatabaseSqlite extends Database {
/**
* Use MySQL's naming (accounts for prefix etc) but remove surrounding backticks
*/
- function tableName($name) {
- return str_replace('`','',parent::tableName($name));
+ function tableName( $name ) {
+ return str_replace( '`', '', parent::tableName( $name ) );
}
/**
@@ -170,20 +222,26 @@ class DatabaseSqlite extends Database {
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 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";
+ if ( !is_object( $this->mConn ) )
+ return "Cannot return last error, no db connection";
$e = $this->mConn->errorInfo();
- return isset($e[2]) ? $e[2] : '';
+ return isset( $e[2] ) ? $e[2] : '';
}
function lastErrno() {
- if (!is_object($this->mConn)) {
+ if ( !is_object( $this->mConn ) ) {
return "Cannot return last error, no db connection";
} else {
$info = $this->mConn->errorInfo();
@@ -200,7 +258,7 @@ class DatabaseSqlite extends Database {
* Returns false if the index does not exist
* - if errors are explicitly ignored, returns NULL on failure
*/
- function indexInfo($table, $index, $fname = 'Database::indexExists') {
+ function indexInfo( $table, $index, $fname = 'DatabaseSqlite::indexExists' ) {
$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
$res = $this->query( $sql, $fname );
if ( !$res ) {
@@ -216,8 +274,8 @@ class DatabaseSqlite extends Database {
return $info;
}
- function indexUnique($table, $index, $fname = 'Database::indexUnique') {
- $row = $this->selectRow( 'sqlite_master', '*',
+ function indexUnique( $table, $index, $fname = 'DatabaseSqlite::indexUnique' ) {
+ $row = $this->selectRow( 'sqlite_master', '*',
array(
'type' => 'index',
'name' => $this->indexName( $index ),
@@ -239,67 +297,81 @@ class DatabaseSqlite extends Database {
/**
* 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);
+ 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
+ * Based on generic 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);
+ 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';
+ 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])) {
+ 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;
+ 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 );
}
- 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 '';
+ function replace( $table, $uniqueIndexes, $rows, $fname = 'DatabaseSqlite::replace' ) {
+ if ( !count( $rows ) ) return true;
+
+ # SQLite can't handle multi-row replaces, so divide up into multiple single-row queries
+ if ( isset( $rows[0] ) && is_array( $rows[0] ) ) {
+ $ret = true;
+ foreach ( $rows as $k => $v )
+ if ( !parent::replace( $table, $uniqueIndexes, $v, "$fname/multi-row" ) )
+ $ret = false;
+ } else {
+ $ret = parent::replace( $table, $uniqueIndexes, $rows, "$fname/single-row" );
+ }
+
+ return $ret;
}
/**
* Returns the size of a text field, or -1 for "unlimited"
* In SQLite this is SQLITE_MAX_LENGTH, by default 1GB. No way to query it though.
*/
- function textFieldSize($table, $field) {
- return -1;
+ function textFieldSize( $table, $field ) {
+ return - 1;
}
- /**
- * No low priority option in SQLite
- */
- function lowPriorityOption() {
- return '';
+ function unionSupportsOrderAndLimit() {
+ return false;
}
- /**
- * 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 unionQueries( $sqls, $all ) {
+ $glue = $all ? ' UNION ALL ' : ' UNION ';
+ return implode( $glue, $sqls );
}
function wasDeadlock() {
- return $this->lastErrno() == SQLITE_BUSY;
+ return $this->lastErrno() == 5; // SQLITE_BUSY
}
function wasErrorReissuable() {
- return $this->lastErrno() == SQLITE_SCHEMA;
+ return $this->lastErrno() == 17; // SQLITE_SCHEMA;
+ }
+
+ function wasReadOnlyError() {
+ return $this->lastErrno() == 8; // SQLITE_READONLY;
}
/**
@@ -313,15 +385,14 @@ class DatabaseSqlite extends Database {
* @return string Version information from the database
*/
function getServerVersion() {
- global $wgContLang;
- $ver = $this->mConn->getAttribute(PDO::ATTR_SERVER_VERSION);
+ $ver = $this->mConn->getAttribute( PDO::ATTR_SERVER_VERSION );
return $ver;
}
/**
* Query whether a given column exists in the mediawiki schema
*/
- function fieldExists($table, $field, $fname = '') {
+ function fieldExists( $table, $field, $fname = '' ) {
$info = $this->fieldInfo( $table, $field );
return (bool)$info;
}
@@ -330,7 +401,7 @@ class DatabaseSqlite extends Database {
* Get information about a given field
* Returns false if the field does not exist.
*/
- function fieldInfo($table, $field) {
+ function fieldInfo( $table, $field ) {
$tableName = $this->tableName( $table );
$sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
$res = $this->query( $sql, __METHOD__ );
@@ -343,73 +414,60 @@ class DatabaseSqlite extends Database {
}
function begin( $fname = '' ) {
- if ($this->mTrxLevel == 1) $this->commit();
+ if ( $this->mTrxLevel == 1 ) $this->commit();
$this->mConn->beginTransaction();
$this->mTrxLevel = 1;
}
function commit( $fname = '' ) {
- if ($this->mTrxLevel == 0) return;
+ if ( $this->mTrxLevel == 0 ) return;
$this->mConn->commit();
$this->mTrxLevel = 0;
}
function rollback( $fname = '' ) {
- if ($this->mTrxLevel == 0) return;
+ if ( $this->mTrxLevel == 0 ) return;
$this->mConn->rollBack();
$this->mTrxLevel = 0;
}
- function limitResultForUpdate($sql, $num) {
+ function limitResultForUpdate( $sql, $num ) {
return $this->limitResult( $sql, $num );
}
- function strencode($s) {
- return substr($this->addQuotes($s),1,-1);
+ function strencode( $s ) {
+ return substr( $this->addQuotes( $s ), 1, - 1 );
}
- function encodeBlob($b) {
+ function encodeBlob( $b ) {
return new Blob( $b );
}
- function decodeBlob($b) {
- if ($b instanceof Blob) {
+ function decodeBlob( $b ) {
+ if ( $b instanceof Blob ) {
$b = $b->fetch();
}
return $b;
}
- function addQuotes($s) {
+ function addQuotes( $s ) {
if ( $s instanceof Blob ) {
return "x'" . bin2hex( $s->fetch() ) . "'";
} else {
- return $this->mConn->quote($s);
+ return $this->mConn->quote( $s );
}
}
- function quote_ident($s) { return $s; }
-
- /**
- * Not possible in SQLite
- * We have ATTACH_DATABASE but that requires database selectors before the
- * table names and in any case is really a different concept to MySQL's USE
- */
- function selectDB($db) {
- if ( $db != $this->mName ) {
- throw new MWException( 'selectDB is not implemented in SQLite' );
- }
+ function quote_ident( $s ) {
+ return $s;
}
- /**
- * not done
- */
- public function setTimeout($timeout) { return; }
-
- /**
- * No-op for a non-networked database
- */
- function ping() {
- return true;
+ function buildLike() {
+ $params = func_get_args();
+ if ( count( $params ) > 0 && is_array( $params[0] ) ) {
+ $params = $params[0];
+ }
+ return parent::buildLike( $params ) . "ESCAPE '\' ";
}
/**
@@ -424,40 +482,33 @@ class DatabaseSqlite extends Database {
* - 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 = '';
+ global $IP;
# Process common MySQL/SQLite table definitions
$err = $this->sourceFile( "$IP/maintenance/tables.sql" );
- if ($err !== true) {
- $this->reportQueryError($err,0,$sql,__FUNCTION__);
- exit( 1 );
+ if ( $err !== true ) {
+ echo " <b>FAILED</b></li>";
+ dieout( htmlspecialchars( $err ) );
}
+ echo " done.</li>";
# Use DatabasePostgres's code to populate interwiki from MySQL template
- $f = fopen("$IP/maintenance/interwiki.sql",'r');
- if ($f == false) dieout("<li>Could not find the interwiki.sql file");
+ $f = fopen( "$IP/maintenance/interwiki.sql", 'r' );
+ if ( $f == false ) {
+ dieout( "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);
+ while ( !feof( $f ) ) {
+ $line = fgets( $f, 1024 );
$matches = array();
- if (!preg_match('/^\s*(\(.+?),(\d)\)/', $line, $matches)) continue;
- $this->query("$sql $matches[1],$matches[2])");
+ 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;
- }
-
public function getSearchEngine() {
- return "SearchEngineDummy";
+ return "SearchSqlite";
}
/**
@@ -471,23 +522,33 @@ class DatabaseSqlite extends Database {
protected function replaceVars( $s ) {
$s = parent::replaceVars( $s );
- if ( preg_match( '/^\s*CREATE TABLE/i', $s ) ) {
+ if ( preg_match( '/^\s*(CREATE|ALTER) TABLE/i', $s ) ) {
// CREATE TABLE hacks to allow schema file sharing with MySQL
-
+
// binary/varbinary column type -> blob
- $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'blob\1', $s );
+ $s = preg_replace( '/\b(var)?binary(\(\d+\))/i', 'BLOB', $s );
// no such thing as unsigned
- $s = preg_replace( '/\bunsigned\b/i', '', $s );
- // INT -> INTEGER for primary keys
- $s = preg_replacE( '/\bint\b/i', 'integer', $s );
+ $s = preg_replace( '/\b(un)?signed\b/i', '', $s );
+ // INT -> INTEGER
+ $s = preg_replace( '/\b(tiny|small|medium|big|)int(\([\s\d]*\)|\b)/i', 'INTEGER', $s );
+ // varchar -> TEXT
+ $s = preg_replace( '/\bvarchar\(\d+\)/i', 'TEXT', $s );
+ // TEXT normalization
+ $s = preg_replace( '/\b(tiny|medium|long)text\b/i', 'TEXT', $s );
+ // BLOB normalization
+ $s = preg_replace( '/\b(tiny|small|medium|long|)blob\b/i', 'BLOB', $s );
+ // BOOL -> INTEGER
+ $s = preg_replace( '/\bbool(ean)?\b/i', 'INTEGER', $s );
+ // DATETIME -> TEXT
+ $s = preg_replace( '/\b(datetime|timestamp)\b/i', 'TEXT', $s );
// No ENUM type
- $s = preg_replace( '/enum\([^)]*\)/i', 'blob', $s );
+ $s = preg_replace( '/enum\([^)]*\)/i', 'BLOB', $s );
// binary collation type -> nothing
$s = preg_replace( '/\bbinary\b/i', '', $s );
// auto_increment -> autoincrement
- $s = preg_replace( '/\bauto_increment\b/i', 'autoincrement', $s );
+ $s = preg_replace( '/\bauto_increment\b/i', 'AUTOINCREMENT', $s );
// No explicit options
- $s = preg_replace( '/\)[^)]*$/', ')', $s );
+ $s = preg_replace( '/\)[^);]*(;?)\s*$/', ')\1', $s );
} elseif ( preg_match( '/^\s*CREATE (\s*(?:UNIQUE|FULLTEXT)\s+)?INDEX/i', $s ) ) {
// No truncated indexes
$s = preg_replace( '/\(\d+\)/', '', $s );
@@ -504,9 +565,31 @@ class DatabaseSqlite extends Database {
return '(' . implode( ') || (', $stringList ) . ')';
}
+ function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = 'DatabaseSqlite::duplicateTableStructure' ) {
+ $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name='$oldName' AND type='table'", $fname );
+ $obj = $this->fetchObject( $res );
+ if ( !$obj ) {
+ throw new MWException( "Couldn't retrieve structure for table $oldName" );
+ }
+ $sql = $obj->sql;
+ $sql = preg_replace( '/\b' . preg_quote( $oldName ) . '\b/', $newName, $sql, 1 );
+ return $this->query( $sql, $fname );
+ }
+
} // end DatabaseSqlite class
/**
+ * This class allows simple acccess to a SQLite database independently from main database settings
+ * @ingroup Database
+ */
+class DatabaseSqliteStandalone extends DatabaseSqlite {
+ public function __construct( $fileName, $flags = 0 ) {
+ $this->mFlags = $flags;
+ $this->openFile( $fileName );
+ }
+}
+
+/**
* @ingroup Database
*/
class SQLiteField {
@@ -545,10 +628,9 @@ class SQLiteField {
# isKey(), isMultipleKey() not implemented, MySQL-specific concept.
# Suggest removal from base class [TS]
-
+
function type() {
return $this->info->type;
}
} // end SQLiteField
-
diff --git a/includes/db/LBFactory.php b/includes/db/LBFactory.php
index 3876d71f..10c87133 100644
--- a/includes/db/LBFactory.php
+++ b/includes/db/LBFactory.php
@@ -25,7 +25,7 @@ abstract class LBFactory {
/**
* Shut down, close connections and destroy the cached instance.
- *
+ *
*/
static function destroyInstance() {
if ( self::$instance ) {
@@ -41,7 +41,7 @@ abstract class LBFactory {
abstract function __construct( $conf );
/**
- * Create a new load balancer object. The resulting object will be untracked,
+ * 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
@@ -58,8 +58,8 @@ abstract class LBFactory {
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
+ * 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
@@ -142,8 +142,8 @@ class LBFactory_Simple extends LBFactory {
}
return new LoadBalancer( array(
- 'servers' => $servers,
- 'masterWaitTimeout' => $wgMasterWaitTimeout
+ 'servers' => $servers,
+ 'masterWaitTimeout' => $wgMasterWaitTimeout
));
}
@@ -162,7 +162,7 @@ class LBFactory_Simple extends LBFactory {
throw new MWException( __METHOD__.": Unknown cluster \"$cluster\"" );
}
return new LoadBalancer( array(
- 'servers' => $wgExternalServers[$cluster]
+ 'servers' => $wgExternalServers[$cluster]
));
}
diff --git a/includes/db/LoadBalancer.php b/includes/db/LoadBalancer.php
index 0b8ef05a..083b70b3 100644
--- a/includes/db/LoadBalancer.php
+++ b/includes/db/LoadBalancer.php
@@ -809,7 +809,7 @@ class LoadBalancer {
foreach ( $this->mConns as $conns2 ) {
foreach ( $conns2 as $conns3 ) {
foreach ( $conns3 as $conn ) {
- $conn->immediateCommit();
+ $conn->commit();
}
}
}
@@ -831,7 +831,7 @@ class LoadBalancer {
}
}
- function waitTimeout( $value = NULL ) {
+ function waitTimeout( $value = null ) {
return wfSetVar( $this->mWaitTimeout, $value );
}
@@ -878,14 +878,18 @@ class LoadBalancer {
* 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.
+ * @param $wiki string Wiki ID, or false for the default database
*/
- function getMaxLag() {
+ function getMaxLag( $wiki = false ) {
$maxLag = -1;
$host = '';
foreach ( $this->mServers as $i => $conn ) {
- $conn = $this->getAnyOpenConnection( $i );
+ $conn = false;
+ if ( $wiki === false ) {
+ $conn = $this->getAnyOpenConnection( $i );
+ }
if ( !$conn ) {
- $conn = $this->openConnection( $i );
+ $conn = $this->openConnection( $i, $wiki );
}
if ( !$conn ) {
continue;
@@ -912,4 +916,11 @@ class LoadBalancer {
$this->mLagTimes = $this->getLoadMonitor()->getLagTimes( array_keys( $this->mServers ), $wiki );
return $this->mLagTimes;
}
+
+ /**
+ * Clear the cache for getLagTimes
+ */
+ function clearLagTimeCache() {
+ $this->mLagTimes = null;
+ }
}
diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php
index aa48f9f3..184d1fc2 100644
--- a/includes/diff/DifferenceEngine.php
+++ b/includes/diff/DifferenceEngine.php
@@ -3,939 +3,6 @@
* @defgroup DifferenceEngine DifferenceEngine
*/
-/**
- * Constant to indicate diff cache compatibility.
- * Bump this when changing the diff formatting in a way that
- * fixes important bugs or such to force cached diff views to
- * clear.
- */
-define( 'MW_DIFF_VERSION', '1.11a' );
-
-/**
- * @todo document
- * @ingroup DifferenceEngine
- */
-class DifferenceEngine {
- /**#@+
- * @private
- */
- var $mOldid, $mNewid, $mTitle;
- var $mOldtitle, $mNewtitle, $mPagetitle;
- var $mOldtext, $mNewtext;
- var $mOldPage, $mNewPage;
- var $mRcidMarkPatrolled;
- var $mOldRev, $mNewRev;
- var $mRevisionsLoaded = false; // Have the revisions been loaded
- var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
- var $mCacheHit = false; // Was the diff fetched from cache?
- var $htmldiff;
-
- protected $unhide = false;
- /**#@-*/
-
- /**
- * Constructor
- * @param $titleObj Title object that the diff is associated with
- * @param $old Integer: old ID we want to show and diff with.
- * @param $new String: either 'prev' or 'next'.
- * @param $rcid Integer: ??? FIXME (default 0)
- * @param $refreshCache boolean If set, refreshes the diff cache
- * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff
- * @param $unhide boolean If set, allow viewing deleted revs
- */
- function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false, $unhide = false ) {
- $this->mTitle = $titleObj;
- wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
-
- if ( 'prev' === $new ) {
- # Show diff between revision $old and the previous one.
- # Get previous one from DB.
- $this->mNewid = intval($old);
- $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
- } elseif ( 'next' === $new ) {
- # Show diff between revision $old and the next one.
- # Get next one from DB.
- $this->mOldid = intval($old);
- $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
- if ( false === $this->mNewid ) {
- # if no result, NewId points to the newest old revision. The only newer
- # revision is cur, which is "0".
- $this->mNewid = 0;
- }
- } else {
- $this->mOldid = intval($old);
- $this->mNewid = intval($new);
- wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) );
- }
- $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
- $this->mRefreshCache = $refreshCache;
- $this->htmldiff = $htmldiff;
- $this->unhide = $unhide;
- }
-
- function getTitle() {
- return $this->mTitle;
- }
-
- function wasCacheHit() {
- return $this->mCacheHit;
- }
-
- function getOldid() {
- return $this->mOldid;
- }
-
- function getNewid() {
- return $this->mNewid;
- }
-
- function showDiffPage( $diffOnly = false ) {
- global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
- wfProfileIn( __METHOD__ );
-
-
- # If external diffs are enabled both globally and for the user,
- # we'll use the application/x-external-editor interface to call
- # an external diff tool like kompare, kdiff3, etc.
- if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
- global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
- $wgOut->disable();
- header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
- $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
- $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
- $special=$wgLang->getNsText(NS_SPECIAL);
- $control=<<<CONTROL
- [Process]
- Type=Diff text
- Engine=MediaWiki
- Script={$wgServer}{$wgScript}
- Special namespace={$special}
-
- [File]
- Extension=wiki
- URL=$url1
-
- [File 2]
- Extension=wiki
- URL=$url2
-CONTROL;
- echo($control);
- return;
- }
-
- $wgOut->setArticleFlag( false );
- if ( !$this->loadRevisionData() ) {
- $t = $this->mTitle->getPrefixedText();
- $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
- $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
-
- if ( $this->mNewRev->isCurrent() ) {
- $wgOut->setArticleFlag( true );
- }
-
- # mOldid is false if the difference engine is called with a "vague" query for
- # a diff between a version V and its previous version V' AND the version V
- # is the first version of that article. In that case, V' does not exist.
- if ( $this->mOldid === false ) {
- $this->showFirstRevision();
- $this->renderNewRevision(); // should we respect $diffOnly here or not?
- wfProfileOut( __METHOD__ );
- return;
- }
-
- $wgOut->suppressQuickbar();
-
- $oldTitle = $this->mOldPage->getPrefixedText();
- $newTitle = $this->mNewPage->getPrefixedText();
- if( $oldTitle == $newTitle ) {
- $wgOut->setPageTitle( $newTitle );
- } else {
- $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
- }
- $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
- if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
- $wgOut->loginToUse();
- $wgOut->output();
- $wgOut->disable();
- wfProfileOut( __METHOD__ );
- return;
- }
-
- $sk = $wgUser->getSkin();
-
- // 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 && $this->mTitle->userCan('patrol') ) {
- // If we've been given an explicit change identifier, use it; saves time
- if( $this->mRcidMarkPatrolled ) {
- $rcid = $this->mRcidMarkPatrolled;
- $rc = RecentChange::newFromId( $rcid );
- // Already patrolled?
- $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
- } else {
- // Look for an unpatrolled change corresponding to this diff
- $db = wfGetDB( DB_SLAVE );
- $change = RecentChange::newFromConds(
- array(
- // Add redundant user,timestamp condition so we can use the existing index
- 'rc_user_text' => $this->mNewRev->getRawUserText(),
- 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
- 'rc_this_oldid' => $this->mNewid,
- 'rc_last_oldid' => $this->mOldid,
- 'rc_patrolled' => 0
- ),
- __METHOD__
- );
- if( $change instanceof RecentChange ) {
- $rcid = $change->mAttribs['rc_id'];
- $this->mRcidMarkPatrolled = $rcid;
- } else {
- // None found
- $rcid = 0;
- }
- }
- // Build the link
- if( $rcid ) {
- $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj( $this->mTitle,
- wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$rcid}" ) . ']</span>';
- } else {
- $patrol = '';
- }
- } else {
- $patrol = '';
- }
-
- $diffOnlyArg = '';
- # Carry over 'diffonly' param via navigation links
- if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
- $diffOnlyArg = '&diffonly='.$diffOnly;
- }
- $htmldiffarg = $this->htmlDiffArgument();
- # Make "previous revision link"
- $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
- "diff=prev&oldid={$this->mOldid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-prevlink"' );
- # Make "next revision link"
- if( $this->mNewRev->isCurrent() ) {
- $nextlink = '&nbsp;';
- } else {
- $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
- "diff=next&oldid={$this->mNewid}{$htmldiffarg}{$diffOnlyArg}", '', '', 'id="differences-nextlink"' );
- }
-
- $oldminor = '';
- $newminor = '';
-
- if( $this->mOldRev->isMinor() ) {
- $oldminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
- }
- if( $this->mNewRev->isMinor() ) {
- $newminor = Xml::span( wfMsg( 'minoreditletter' ), 'minor' ) . ' ';
- }
-
- $rdel = ''; $ldel = '';
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
- if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $ldel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
- } else {
- $query = array( 'target' => $this->mOldRev->mTitle->getPrefixedDbkey(),
- 'oldid' => $this->mOldRev->getId()
- );
- $ldel = $sk->revDeleteLink( $query, $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) );
- }
- $ldel = "&nbsp;&nbsp;&nbsp;$ldel ";
- // We don't currently handle well changing the top revision's settings
- if( $this->mNewRev->isCurrent() ) {
- $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
- } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $rdel = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml( 'rev-delundel' ).')' );
- } else {
- $query = array( 'target' => $this->mNewRev->mTitle->getPrefixedDbkey(),
- 'oldid' => $this->mNewRev->getId()
- );
- $rdel = $sk->revDeleteLink( $query, $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) );
- }
- $rdel = "&nbsp;&nbsp;&nbsp;$rdel ";
- }
-
- $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
- '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, !$this->unhide ) . "</div>" .
- '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel."</div>" .
- '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
- $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
- '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . " $rollback</div>" .
- '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel."</div>" .
- '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
-
- # Check if this user can see the revisions
- $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
- && $this->mNewRev->userCan(Revision::DELETED_TEXT);
- $deleted = $this->mOldRev->isDeleted(Revision::DELETED_TEXT)
- || $this->mNewRev->isDeleted(Revision::DELETED_TEXT);
- # Output the diff if allowed...
- if( $deleted && (!$this->unhide || !$allowed) ) {
- $this->showDiffStyle();
- $multi = $this->getMultiNotice();
- $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
- if( !$allowed ) {
- # Give explanation for why revision is not visible
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
- array( 'rev-deleted-no-diff' ) );
- } else {
- # Give explanation and add a link to view the diff...
- $link = $this->mTitle->getFullUrl( "diff={$this->mNewid}&oldid={$this->mOldid}".
- '&unhide=1&token='.urlencode( $wgUser->editToken($this->mNewid) ) );
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
- array( 'rev-deleted-unhide-diff', $link ) );
- }
- } else if( $wgEnableHtmlDiff && $this->htmldiff ) {
- $multi = $this->getMultiNotice();
- $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
- 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
- $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
- $this->renderHtmlDiff();
- } else {
- if( $wgEnableHtmlDiff ) {
- $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
- 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
- }
- $this->showDiff( $oldHeader, $newHeader );
- if( !$diffOnly ) {
- $this->renderNewRevision();
- }
- }
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Show the new revision of the page.
- */
- function renderNewRevision() {
- global $wgOut, $wgUser;
- wfProfileIn( __METHOD__ );
-
- $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
- # Add deleted rev tag if needed
- if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
- } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
- }
-
- if( !$this->mNewRev->isCurrent() ) {
- $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
- }
-
- $this->loadNewText();
- if( is_object( $this->mNewRev ) ) {
- $wgOut->setRevisionId( $this->mNewRev->getId() );
- }
-
- if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
- // Stolen from Article::view --AG 2007-10-11
- // Give hooks a chance to customise the output
- if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
- // Wrap the whole lot in a <pre> and don't parse
- $m = array();
- preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
- $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
- $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
- $wgOut->addHTML( "\n</pre>\n" );
- }
- } else {
- $wgOut->addWikiTextTidy( $this->mNewtext );
- }
-
- if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
- $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
- }
- # Add redundant patrol link on bottom...
- if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
- $sk = $wgUser->getSkin();
- $wgOut->addHTML(
- "<div class='patrollink'>[" . $sk->makeKnownLinkObj( $this->mTitle,
- wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$this->mRcidMarkPatrolled}" ) .
- ']</div>'
- );
- }
-
- wfProfileOut( __METHOD__ );
- }
-
-
- function renderHtmlDiff() {
- global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
- wfProfileIn( __METHOD__ );
-
- $this->showDiffStyle();
-
- $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
- #add deleted rev tag if needed
- if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
- } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
- $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
- }
-
- if( !$this->mNewRev->isCurrent() ) {
- $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
- }
-
- $this->loadText();
-
- // Old revision
- if( is_object( $this->mOldRev ) ) {
- $wgOut->setRevisionId( $this->mOldRev->getId() );
- }
-
- $popts = $wgOut->parserOptions();
- $oldTidy = $popts->setTidy( true );
- $popts->setEditSection( false );
-
- $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
- $popts->setTidy( $oldTidy );
-
- //only for new?
- //$wgOut->addParserOutputNoText( $parserOutput );
- $oldHtml = $parserOutput->getText();
- wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
-
- // New revision
- if( is_object( $this->mNewRev ) ) {
- $wgOut->setRevisionId( $this->mNewRev->getId() );
- }
-
- $popts = $wgOut->parserOptions();
- $oldTidy = $popts->setTidy( true );
-
- $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
- $popts->setTidy( $oldTidy );
-
- $wgOut->addParserOutputNoText( $parserOutput );
- $newHtml = $parserOutput->getText();
- wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
-
- unset($parserOutput, $popts);
-
- $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
- $differ->htmlDiff($oldHtml, $newHtml);
- if ( $wgDebugComments ) {
- $wgOut->addHTML( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
- }
-
- wfProfileOut( __METHOD__ );
- }
-
- /**
- * Show the first revision of an article. Uses normal diff headers in
- * contrast to normal "old revision" display style.
- */
- function showFirstRevision() {
- global $wgOut, $wgUser;
- wfProfileIn( __METHOD__ );
-
- # Get article text from the DB
- #
- if ( ! $this->loadNewText() ) {
- $t = $this->mTitle->getPrefixedText();
- $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
- $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
- $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
- wfProfileOut( __METHOD__ );
- return;
- }
- if ( $this->mNewRev->isCurrent() ) {
- $wgOut->setArticleFlag( true );
- }
-
- # Check if user is allowed to look at this page. If not, bail out.
- #
- if ( !$this->mTitle->userCanRead() ) {
- $wgOut->loginToUse();
- $wgOut->output();
- wfProfileOut( __METHOD__ );
- throw new MWException("Permission Error: you do not have access to view this page");
- }
-
- # Prepare the header box
- #
- $sk = $wgUser->getSkin();
-
- $next = $this->mTitle->getNextRevisionID( $this->mNewid );
- if( !$next ) {
- $nextlink = '';
- } else {
- $nextlink = '<br/>' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
- 'diff=next&oldid=' . $this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
- }
- $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
- $sk->revUserTools( $this->mNewRev ) . "<br/>" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
-
- $wgOut->addHTML( $header );
-
- $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
- wfProfileOut( __METHOD__ );
- }
-
- function htmlDiffArgument(){
- global $wgEnableHtmlDiff;
- if($wgEnableHtmlDiff){
- if($this->htmldiff){
- return '&htmldiff=1';
- }else{
- return '&htmldiff=0';
- }
- }else{
- return '';
- }
- }
-
- /**
- * Get the diff text, send it to $wgOut
- * Returns false if the diff could not be generated, otherwise returns true
- */
- function showDiff( $otitle, $ntitle ) {
- global $wgOut;
- $diff = $this->getDiff( $otitle, $ntitle );
- if ( $diff === false ) {
- $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
- return false;
- } else {
- $this->showDiffStyle();
- $wgOut->addHTML( $diff );
- return true;
- }
- }
-
- /**
- * Add style sheets and supporting JS for diff display.
- */
- function showDiffStyle() {
- global $wgStylePath, $wgStyleVersion, $wgOut;
- $wgOut->addStyle( 'common/diff.css' );
-
- // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
- $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
- }
-
- /**
- * Get complete diff table, including header
- *
- * @param Title $otitle Old title
- * @param Title $ntitle New title
- * @return mixed
- */
- function getDiff( $otitle, $ntitle ) {
- $body = $this->getDiffBody();
- if ( $body === false ) {
- return false;
- } else {
- $multi = $this->getMultiNotice();
- return $this->addHeader( $body, $otitle, $ntitle, $multi );
- }
- }
-
- /**
- * Get the diff table body, without header
- *
- * @return mixed
- */
- function getDiffBody() {
- global $wgMemc;
- wfProfileIn( __METHOD__ );
- $this->mCacheHit = true;
- // Check if the diff should be hidden from this user
- if ( !$this->loadRevisionData() )
- return '';
- if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
- return '';
- } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
- return '';
- } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
- return '';
- }
- // Cacheable?
- $key = false;
- if ( $this->mOldid && $this->mNewid ) {
- $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
- // Try cache
- if ( !$this->mRefreshCache ) {
- $difftext = $wgMemc->get( $key );
- if ( $difftext ) {
- wfIncrStats( 'diff_cache_hit' );
- $difftext = $this->localiseLineNumbers( $difftext );
- $difftext .= "\n<!-- diff cache key $key -->\n";
- wfProfileOut( __METHOD__ );
- return $difftext;
- }
- } // don't try to load but save the result
- }
- $this->mCacheHit = false;
-
- // Loadtext is permission safe, this just clears out the diff
- if ( !$this->loadText() ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
-
- $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
-
- // Save to cache for 7 days
- if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
- wfIncrStats( 'diff_uncacheable' );
- } else if ( $key !== false && $difftext !== false ) {
- wfIncrStats( 'diff_cache_miss' );
- $wgMemc->set( $key, $difftext, 7*86400 );
- } else {
- wfIncrStats( 'diff_uncacheable' );
- }
- // Replace line numbers with the text in the user's language
- if ( $difftext !== false ) {
- $difftext = $this->localiseLineNumbers( $difftext );
- }
- wfProfileOut( __METHOD__ );
- return $difftext;
- }
-
- /**
- * Generate a diff, no caching
- * $otext and $ntext must be already segmented
- */
- function generateDiffBody( $otext, $ntext ) {
- global $wgExternalDiffEngine, $wgContLang;
-
- $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
- $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
- $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
- if( !function_exists( 'wikidiff_do_diff' ) ) {
- dl('php_wikidiff.so');
- }
- 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( __METHOD__ . "-dl" );
- @dl('php_wikidiff2.so');
- 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;
- }
- }
- if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
- # Diff via the shell
- global $wgTmpDirectory;
- $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
- $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
-
- $tempFile1 = fopen( $tempName1, "w" );
- if ( !$tempFile1 ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
- $tempFile2 = fopen( $tempName2, "w" );
- if ( !$tempFile2 ) {
- wfProfileOut( __METHOD__ );
- return false;
- }
- fwrite( $tempFile1, $otext );
- fwrite( $tempFile2, $ntext );
- fclose( $tempFile1 );
- fclose( $tempFile2 );
- $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
- wfProfileIn( __METHOD__ . "-shellexec" );
- $difftext = wfShellExec( $cmd );
- $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 ) ) .
- $this->debug();
- }
-
- /**
- * Generate a debug comment indicating diff generating time,
- * server node, and generator backend.
- */
- protected function debug( $generator="internal" ) {
- global $wgShowHostnames;
- $data = array( $generator );
- if( $wgShowHostnames ) {
- $data[] = wfHostname();
- }
- $data[] = wfTimestamp( TS_DB );
- return "<!-- diff generator: " .
- implode( " ",
- array_map(
- "htmlspecialchars",
- $data ) ) .
- " -->\n";
- }
-
- /**
- * Replace line numbers with the text in the user's language
- */
- function localiseLineNumbers( $text ) {
- return preg_replace_callback( '/<!--LINE (\d+)-->/',
- array( &$this, 'localiseLineNumbersCb' ), $text );
- }
-
- function localiseLineNumbersCb( $matches ) {
- global $wgLang;
- return wfMsgExt( 'lineno', array (), $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 ) {
- $tmp = $oldid; $oldid = $newid; $newid = $tmp;
- }
-
- $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
- if ( !$n )
- return '';
-
- return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
- }
-
-
- /**
- * Add the header to a diff body
- */
- static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
- $header = "
- <table class='diff'>
- <col class='diff-marker' />
- <col class='diff-content' />
- <col class='diff-marker' />
- <col class='diff-content' />
- <tr valign='top'>
- <td colspan='2' class='diff-otitle'>{$otitle}</td>
- <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
- </tr>
- ";
-
- if ( $multi != '' )
- $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
-
- return $header . $diff . "</table>";
- }
-
- /**
- * Use specified text instead of loading from the database
- */
- function setText( $oldText, $newText ) {
- $this->mOldtext = $oldText;
- $this->mNewtext = $newText;
- $this->mTextLoaded = 2;
- $this->mRevisionsLoaded = true;
- }
-
- /**
- * Load revision metadata for the specified articles. If newid is 0, then compare
- * the old article in oldid to the current article; if oldid is 0, then
- * compare the current article to the immediately previous one (ignoring the
- * value of newid).
- *
- * If oldid is false, leave the corresponding revision object set
- * to false. This is impossible via ordinary user input, and is provided for
- * API convenience.
- */
- function loadRevisionData() {
- global $wgLang, $wgUser;
- if ( $this->mRevisionsLoaded ) {
- return true;
- } else {
- // Whether it succeeds or fails, we don't want to try again
- $this->mRevisionsLoaded = true;
- }
-
- // Load the new revision object
- $this->mNewRev = $this->mNewid
- ? Revision::newFromId( $this->mNewid )
- : Revision::newFromTitle( $this->mTitle );
- if( !$this->mNewRev instanceof Revision )
- return false;
-
- // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
- $this->mNewid = $this->mNewRev->getId();
-
- // 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( 'oldid=' . $this->mNewid );
- $this->mPagetitle = wfMsgHTML( 'currentrev-asof', $timestamp );
- $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
-
- $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
- $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>";
- $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>";
- } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
- $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
- }
-
- // Load the old revision object
- $this->mOldRev = false;
- if( $this->mOldid ) {
- $this->mOldRev = Revision::newFromId( $this->mOldid );
- } elseif ( $this->mOldid === 0 ) {
- $rev = $this->mNewRev->getPrevious();
- if( $rev ) {
- $this->mOldid = $rev->getId();
- $this->mOldRev = $rev;
- } else {
- // No previous revision; mark to show as first-version only.
- $this->mOldid = false;
- $this->mOldRev = false;
- }
- }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
-
- if( is_null( $this->mOldRev ) ) {
- return false;
- }
-
- if ( $this->mOldRev ) {
- $this->mOldPage = $this->mOldRev->getTitle();
-
- $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
- $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'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
- // Add an "undo" link
- $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
- $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
- $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
- if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
- $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</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>';
- }
- }
-
- return true;
- }
-
- /**
- * Load the text of the revisions, as well as revision data.
- */
- function loadText() {
- if ( $this->mTextLoaded == 2 ) {
- return true;
- } else {
- // Whether it succeeds or fails, we don't want to try again
- $this->mTextLoaded = 2;
- }
-
- if ( !$this->loadRevisionData() ) {
- return false;
- }
- if ( $this->mOldRev ) {
- $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
- if ( $this->mOldtext === false ) {
- return false;
- }
- }
- if ( $this->mNewRev ) {
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
- if ( $this->mNewtext === false ) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Load the text of the new revision, not the old one
- */
- function loadNewText() {
- if ( $this->mTextLoaded >= 1 ) {
- return true;
- } else {
- $this->mTextLoaded = 1;
- }
- if ( !$this->loadRevisionData() ) {
- return false;
- }
- $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
- return true;
- }
-
-
-}
-
// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
//
// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
diff --git a/includes/diff/DifferenceInterface.php b/includes/diff/DifferenceInterface.php
new file mode 100644
index 00000000..d7d36799
--- /dev/null
+++ b/includes/diff/DifferenceInterface.php
@@ -0,0 +1,1021 @@
+<?php
+/**
+ * @defgroup DifferenceEngine DifferenceEngine
+ */
+
+/**
+ * Constant to indicate diff cache compatibility.
+ * Bump this when changing the diff formatting in a way that
+ * fixes important bugs or such to force cached diff views to
+ * clear.
+ */
+define( 'MW_DIFF_VERSION', '1.11a' );
+
+/**
+ * @todo document
+ * @ingroup DifferenceEngine
+ */
+class DifferenceEngine {
+ /**#@+
+ * @private
+ */
+ var $mOldid, $mNewid, $mTitle;
+ var $mOldtitle, $mNewtitle, $mPagetitle;
+ var $mOldtext, $mNewtext;
+ var $mOldPage, $mNewPage;
+ var $mRcidMarkPatrolled;
+ var $mOldRev, $mNewRev;
+ var $mRevisionsLoaded = false; // Have the revisions been loaded
+ var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
+ var $mCacheHit = false; // Was the diff fetched from cache?
+
+ /**
+ * Set this to true to add debug info to the HTML output.
+ * Warning: this may cause RSS readers to spuriously mark articles as "new"
+ * (bug 20601)
+ */
+ var $enableDebugComment = false;
+
+ // If true, line X is not displayed when X is 1, for example to increase
+ // readability and conserve space with many small diffs.
+ protected $mReducedLineNumbers = false;
+
+ protected $unhide = false;
+ /**#@-*/
+
+ /**
+ * Constructor
+ * @param $titleObj Title object that the diff is associated with
+ * @param $old Integer: old ID we want to show and diff with.
+ * @param $new String: either 'prev' or 'next'.
+ * @param $rcid Integer: ??? FIXME (default 0)
+ * @param $refreshCache boolean If set, refreshes the diff cache
+ * @param $unhide boolean If set, allow viewing deleted revs
+ */
+ function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0,
+ $refreshCache = false, $unhide = false )
+ {
+ if ( $titleObj ) {
+ $this->mTitle = $titleObj;
+ } else {
+ global $wgTitle;
+ $this->mTitle = $wgTitle;
+ }
+ wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
+
+ if ( 'prev' === $new ) {
+ # Show diff between revision $old and the previous one.
+ # Get previous one from DB.
+ $this->mNewid = intval($old);
+ $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
+ } elseif ( 'next' === $new ) {
+ # Show diff between revision $old and the next one.
+ # Get next one from DB.
+ $this->mOldid = intval($old);
+ $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
+ if ( false === $this->mNewid ) {
+ # if no result, NewId points to the newest old revision. The only newer
+ # revision is cur, which is "0".
+ $this->mNewid = 0;
+ }
+ } else {
+ $this->mOldid = intval($old);
+ $this->mNewid = intval($new);
+ wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) );
+ }
+ $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
+ $this->mRefreshCache = $refreshCache;
+ $this->unhide = $unhide;
+ }
+
+ function setReducedLineNumbers( $value = true ) {
+ $this->mReducedLineNumbers = $value;
+ }
+
+ function getTitle() {
+ return $this->mTitle;
+ }
+
+ function wasCacheHit() {
+ return $this->mCacheHit;
+ }
+
+ function getOldid() {
+ return $this->mOldid;
+ }
+
+ function getNewid() {
+ return $this->mNewid;
+ }
+
+ function showDiffPage( $diffOnly = false ) {
+ global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
+ wfProfileIn( __METHOD__ );
+
+
+ # If external diffs are enabled both globally and for the user,
+ # we'll use the application/x-external-editor interface to call
+ # an external diff tool like kompare, kdiff3, etc.
+ if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
+ global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
+ $wgOut->disable();
+ header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
+ $url1=$this->mTitle->getFullURL( array(
+ 'action' => 'raw',
+ 'oldid' => $this->mOldid
+ ) );
+ $url2=$this->mTitle->getFullURL( array(
+ 'action' => 'raw',
+ 'oldid' => $this->mNewid
+ ) );
+ $special=$wgLang->getNsText(NS_SPECIAL);
+ $control=<<<CONTROL
+ [Process]
+ Type=Diff text
+ Engine=MediaWiki
+ Script={$wgServer}{$wgScript}
+ Special namespace={$special}
+
+ [File]
+ Extension=wiki
+ URL=$url1
+
+ [File 2]
+ Extension=wiki
+ URL=$url2
+CONTROL;
+ echo($control);
+ return;
+ }
+
+ $wgOut->setArticleFlag( false );
+ if ( !$this->loadRevisionData() ) {
+ $t = $this->mTitle->getPrefixedText();
+ $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
+ $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
+ $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
+
+ if ( $this->mNewRev->isCurrent() ) {
+ $wgOut->setArticleFlag( true );
+ }
+
+ # mOldid is false if the difference engine is called with a "vague" query for
+ # a diff between a version V and its previous version V' AND the version V
+ # is the first version of that article. In that case, V' does not exist.
+ if ( $this->mOldid === false ) {
+ $this->showFirstRevision();
+ $this->renderNewRevision(); // should we respect $diffOnly here or not?
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $wgOut->suppressQuickbar();
+
+ $oldTitle = $this->mOldPage->getPrefixedText();
+ $newTitle = $this->mNewPage->getPrefixedText();
+ if( $oldTitle == $newTitle ) {
+ $wgOut->setPageTitle( $newTitle );
+ } else {
+ $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
+ }
+ $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+ if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
+ $wgOut->loginToUse();
+ $wgOut->output();
+ $wgOut->disable();
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+
+ $sk = $wgUser->getSkin();
+
+ // 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 && $this->mTitle->userCan('patrol') ) {
+ // If we've been given an explicit change identifier, use it; saves time
+ if( $this->mRcidMarkPatrolled ) {
+ $rcid = $this->mRcidMarkPatrolled;
+ $rc = RecentChange::newFromId( $rcid );
+ // Already patrolled?
+ $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
+ } else {
+ // Look for an unpatrolled change corresponding to this diff
+ $db = wfGetDB( DB_SLAVE );
+ $change = RecentChange::newFromConds(
+ array(
+ // Redundant user,timestamp condition so we can use the existing index
+ 'rc_user_text' => $this->mNewRev->getRawUserText(),
+ 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
+ 'rc_this_oldid' => $this->mNewid,
+ 'rc_last_oldid' => $this->mOldid,
+ 'rc_patrolled' => 0
+ ),
+ __METHOD__
+ );
+ if( $change instanceof RecentChange ) {
+ $rcid = $change->mAttribs['rc_id'];
+ $this->mRcidMarkPatrolled = $rcid;
+ } else {
+ // None found
+ $rcid = 0;
+ }
+ }
+ // Build the link
+ if( $rcid ) {
+ $patrol = ' <span class="patrollink">[' . $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'markaspatrolleddiff' ),
+ array(),
+ array(
+ 'action' => 'markpatrolled',
+ 'rcid' => $rcid
+ ),
+ array(
+ 'known',
+ 'noclasses'
+ )
+ ) . ']</span>';
+ } else {
+ $patrol = '';
+ }
+ } else {
+ $patrol = '';
+ }
+
+ # Carry over 'diffonly' param via navigation links
+ if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
+ $query['diffonly'] = $diffOnly;
+ }
+
+ # Make "previous revision link"
+ $query['diff'] = 'prev';
+ $query['oldid'] = $this->mOldid;
+ # Cascade unhide param in links for easy deletion browsing
+ if( $this->unhide ) {
+ $query['unhide'] = 1;
+ }
+ $prevlink = $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'previousdiff' ),
+ array(
+ 'id' => 'differences-prevlink'
+ ),
+ $query,
+ array(
+ 'known',
+ 'noclasses'
+ )
+ );
+
+ # Make "next revision link"
+ $query['diff'] = 'next';
+ $query['oldid'] = $this->mNewid;
+ # Skip next link on the top revision
+ if( $this->mNewRev->isCurrent() ) {
+ $nextlink = '&nbsp;';
+ } else {
+ $nextlink = $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'nextdiff' ),
+ array(
+ 'id' => 'differences-nextlink'
+ ),
+ $query,
+ array(
+ 'known',
+ 'noclasses'
+ )
+ );
+ }
+
+ $oldminor = '';
+ $newminor = '';
+
+ if( $this->mOldRev->isMinor() ) {
+ $oldminor = ChangesList::flag( 'minor' );
+ }
+ if( $this->mNewRev->isMinor() ) {
+ $newminor = ChangesList::flag( 'minor' );
+ }
+
+ # Handle RevisionDelete links...
+ $ldel = $this->revisionDeleteLink( $this->mOldRev );
+ $rdel = $this->revisionDeleteLink( $this->mNewRev );
+
+ $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
+ '<div id="mw-diff-otitle2">' .
+ $sk->revUserTools( $this->mOldRev, !$this->unhide ).'</div>' .
+ '<div id="mw-diff-otitle3">' . $oldminor .
+ $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel.'</div>' .
+ '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
+ $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
+ '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) .
+ " $rollback</div>" .
+ '<div id="mw-diff-ntitle3">' . $newminor .
+ $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel.'</div>' .
+ '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
+
+ # Check if this user can see the revisions
+ $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
+ && $this->mNewRev->userCan(Revision::DELETED_TEXT);
+ # Check if one of the revisions is deleted/suppressed
+ $deleted = $suppressed = false;
+ if( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
+ $deleted = true; // old revisions text is hidden
+ if( $this->mOldRev->isDeleted(Revision::DELETED_RESTRICTED) )
+ $suppressed = true; // also suppressed
+ }
+ if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+ $deleted = true; // new revisions text is hidden
+ if( $this->mNewRev->isDeleted(Revision::DELETED_RESTRICTED) )
+ $suppressed = true; // also suppressed
+ }
+ # If the diff cannot be shown due to a deleted revision, then output
+ # the diff header and links to unhide (if available)...
+ if( $deleted && (!$this->unhide || !$allowed) ) {
+ $this->showDiffStyle();
+ $multi = $this->getMultiNotice();
+ $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
+ if( !$allowed ) {
+ $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
+ # Give explanation for why revision is not visible
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
+ array( $msg ) );
+ } else {
+ # Give explanation and add a link to view the diff...
+ $link = $this->mTitle->getFullUrl( array(
+ 'diff' => $this->mNewid,
+ 'oldid' => $this->mOldid,
+ 'unhide' => 1
+ ) );
+ $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", array( $msg, $link ) );
+ }
+ # Otherwise, output a regular diff...
+ } else {
+ # Add deletion notice if the user is viewing deleted content
+ $notice = '';
+ if( $deleted ) {
+ $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
+ $notice = "<div class='mw-warning plainlinks'>\n".wfMsgExt($msg,'parseinline')."</div>\n";
+ }
+ $this->showDiff( $oldHeader, $newHeader, $notice );
+ if( !$diffOnly ) {
+ $this->renderNewRevision();
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ protected function revisionDeleteLink( $rev ) {
+ global $wgUser;
+ $link = '';
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ // Show del/undel link if:
+ // (a) the user can delete revisions, or
+ // (b) the user can view deleted revision *and* this one is deleted
+ if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed( 'deletedhistory' )) ) {
+ $sk = $wgUser->getSkin();
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $link = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ } else {
+ $query = array(
+ 'type' => 'revision',
+ 'target' => $rev->mTitle->getPrefixedDbkey(),
+ 'ids' => $rev->getId()
+ );
+ $link = $sk->revDeleteLink( $query,
+ $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
+ }
+ $link = '&nbsp;&nbsp;&nbsp;' . $link . ' ';
+ }
+ return $link;
+ }
+
+ /**
+ * Show the new revision of the page.
+ */
+ function renderNewRevision() {
+ global $wgOut, $wgUser;
+ wfProfileIn( __METHOD__ );
+
+ $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
+ # Add deleted rev tag if needed
+ if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
+ } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+ $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
+ }
+
+ if( !$this->mNewRev->isCurrent() ) {
+ $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
+ }
+
+ $this->loadNewText();
+ if( is_object( $this->mNewRev ) ) {
+ $wgOut->setRevisionId( $this->mNewRev->getId() );
+ }
+
+ if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
+ // Stolen from Article::view --AG 2007-10-11
+ // Give hooks a chance to customise the output
+ if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
+ // Wrap the whole lot in a <pre> and don't parse
+ $m = array();
+ preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+ $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+ $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
+ $wgOut->addHTML( "\n</pre>\n" );
+ }
+ } else {
+ $wgOut->addWikiTextTidy( $this->mNewtext );
+ }
+
+ if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
+ $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
+ }
+ # Add redundant patrol link on bottom...
+ if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
+ $sk = $wgUser->getSkin();
+ $wgOut->addHTML(
+ "<div class='patrollink'>[" . $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'markaspatrolleddiff' ),
+ array(),
+ array(
+ 'action' => 'markpatrolled',
+ 'rcid' => $this->mRcidMarkPatrolled
+ )
+ ) . ']</div>'
+ );
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Show the first revision of an article. Uses normal diff headers in
+ * contrast to normal "old revision" display style.
+ */
+ function showFirstRevision() {
+ global $wgOut, $wgUser;
+ wfProfileIn( __METHOD__ );
+
+ # Get article text from the DB
+ #
+ if ( ! $this->loadNewText() ) {
+ $t = $this->mTitle->getPrefixedText();
+ $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
+ $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
+ $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+ wfProfileOut( __METHOD__ );
+ return;
+ }
+ if ( $this->mNewRev->isCurrent() ) {
+ $wgOut->setArticleFlag( true );
+ }
+
+ # Check if user is allowed to look at this page. If not, bail out.
+ #
+ if ( !$this->mTitle->userCanRead() ) {
+ $wgOut->loginToUse();
+ $wgOut->output();
+ wfProfileOut( __METHOD__ );
+ throw new MWException("Permission Error: you do not have access to view this page");
+ }
+
+ # Prepare the header box
+ #
+ $sk = $wgUser->getSkin();
+
+ $next = $this->mTitle->getNextRevisionID( $this->mNewid );
+ if( !$next ) {
+ $nextlink = '';
+ } else {
+ $nextlink = '<br />' . $sk->link(
+ $this->mTitle,
+ wfMsgHtml( 'nextdiff' ),
+ array(
+ 'id' => 'differences-nextlink'
+ ),
+ array(
+ 'diff' => 'next',
+ 'oldid' => $this->mNewid,
+ ),
+ array(
+ 'known',
+ 'noclasses'
+ )
+ );
+ }
+ $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
+ $sk->revUserTools( $this->mNewRev ) . "<br />" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
+
+ $wgOut->addHTML( $header );
+
+ $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
+ $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * Get the diff text, send it to $wgOut
+ * Returns false if the diff could not be generated, otherwise returns true
+ */
+ function showDiff( $otitle, $ntitle, $notice = '' ) {
+ global $wgOut;
+ $diff = $this->getDiff( $otitle, $ntitle, $notice );
+ if ( $diff === false ) {
+ $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
+ return false;
+ } else {
+ $this->showDiffStyle();
+ $wgOut->addHTML( $diff );
+ return true;
+ }
+ }
+
+ /**
+ * Add style sheets and supporting JS for diff display.
+ */
+ function showDiffStyle() {
+ global $wgStylePath, $wgStyleVersion, $wgOut;
+ $wgOut->addStyle( 'common/diff.css' );
+
+ // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
+ $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
+ }
+
+ /**
+ * Get complete diff table, including header
+ *
+ * @param Title $otitle Old title
+ * @param Title $ntitle New title
+ * @param string $notice HTML between diff header and body
+ * @return mixed
+ */
+ function getDiff( $otitle, $ntitle, $notice = '' ) {
+ $body = $this->getDiffBody();
+ if ( $body === false ) {
+ return false;
+ } else {
+ $multi = $this->getMultiNotice();
+ return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
+ }
+ }
+
+ /**
+ * Get the diff table body, without header
+ *
+ * @return mixed
+ */
+ function getDiffBody() {
+ global $wgMemc;
+ wfProfileIn( __METHOD__ );
+ $this->mCacheHit = true;
+ // Check if the diff should be hidden from this user
+ if ( !$this->loadRevisionData() )
+ return '';
+ if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
+ return '';
+ } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+ return '';
+ } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
+ return '';
+ }
+ // Cacheable?
+ $key = false;
+ if ( $this->mOldid && $this->mNewid ) {
+ $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
+ // Try cache
+ if ( !$this->mRefreshCache ) {
+ $difftext = $wgMemc->get( $key );
+ if ( $difftext ) {
+ wfIncrStats( 'diff_cache_hit' );
+ $difftext = $this->localiseLineNumbers( $difftext );
+ $difftext .= "\n<!-- diff cache key $key -->\n";
+ wfProfileOut( __METHOD__ );
+ return $difftext;
+ }
+ } // don't try to load but save the result
+ }
+ $this->mCacheHit = false;
+
+ // Loadtext is permission safe, this just clears out the diff
+ if ( !$this->loadText() ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+
+ // Save to cache for 7 days
+ if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
+ wfIncrStats( 'diff_uncacheable' );
+ } else if ( $key !== false && $difftext !== false ) {
+ wfIncrStats( 'diff_cache_miss' );
+ $wgMemc->set( $key, $difftext, 7*86400 );
+ } else {
+ wfIncrStats( 'diff_uncacheable' );
+ }
+ // Replace line numbers with the text in the user's language
+ if ( $difftext !== false ) {
+ $difftext = $this->localiseLineNumbers( $difftext );
+ }
+ wfProfileOut( __METHOD__ );
+ return $difftext;
+ }
+
+ /**
+ * Make sure the proper modules are loaded before we try to
+ * make the diff
+ */
+ private function initDiffEngines() {
+ global $wgExternalDiffEngine;
+ if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
+ wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
+ wfSuppressWarnings();
+ dl( 'php_wikidiff.so' );
+ wfRestoreWarnings();
+ wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
+ }
+ else if ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
+ wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
+ wfSuppressWarnings();
+ dl( 'php_wikidiff2.so' );
+ wfRestoreWarnings();
+ wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
+ }
+ }
+
+ /**
+ * Generate a diff, no caching
+ * $otext and $ntext must be already segmented
+ */
+ function generateDiffBody( $otext, $ntext ) {
+ global $wgExternalDiffEngine, $wgContLang;
+
+ $otext = str_replace( "\r\n", "\n", $otext );
+ $ntext = str_replace( "\r\n", "\n", $ntext );
+
+ $this->initDiffEngines();
+
+ if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) {
+ # For historical reasons, external diff engine expects
+ # input text to be HTML-escaped already
+ $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
+ $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
+ return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
+ $this->debug( 'wikidiff1' );
+ }
+
+ if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) {
+ # Better external diff engine, the 2 may some day be dropped
+ # This one does the escaping and segmenting itself
+ wfProfileIn( 'wikidiff2_do_diff' );
+ $text = wikidiff2_do_diff( $otext, $ntext, 2 );
+ $text .= $this->debug( 'wikidiff2' );
+ wfProfileOut( 'wikidiff2_do_diff' );
+ return $text;
+ }
+ if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
+ # Diff via the shell
+ global $wgTmpDirectory;
+ $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
+ $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
+
+ $tempFile1 = fopen( $tempName1, "w" );
+ if ( !$tempFile1 ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ $tempFile2 = fopen( $tempName2, "w" );
+ if ( !$tempFile2 ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+ fwrite( $tempFile1, $otext );
+ fwrite( $tempFile2, $ntext );
+ fclose( $tempFile1 );
+ fclose( $tempFile2 );
+ $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
+ wfProfileIn( __METHOD__ . "-shellexec" );
+ $difftext = wfShellExec( $cmd );
+ $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 ) ) .
+ $this->debug();
+ }
+
+ /**
+ * Generate a debug comment indicating diff generating time,
+ * server node, and generator backend.
+ */
+ protected function debug( $generator="internal" ) {
+ global $wgShowHostnames;
+ if ( !$this->enableDebugComment ) {
+ return '';
+ }
+ $data = array( $generator );
+ if( $wgShowHostnames ) {
+ $data[] = wfHostname();
+ }
+ $data[] = wfTimestamp( TS_DB );
+ return "<!-- diff generator: " .
+ implode( " ",
+ array_map(
+ "htmlspecialchars",
+ $data ) ) .
+ " -->\n";
+ }
+
+ /**
+ * Replace line numbers with the text in the user's language
+ */
+ function localiseLineNumbers( $text ) {
+ return preg_replace_callback( '/<!--LINE (\d+)-->/',
+ array( &$this, 'localiseLineNumbersCb' ), $text );
+ }
+
+ function localiseLineNumbersCb( $matches ) {
+ global $wgLang;
+ if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
+ return wfMsgExt( 'lineno', 'escape', $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 ) {
+ $tmp = $oldid; $oldid = $newid; $newid = $tmp;
+ }
+
+ $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
+ if ( !$n )
+ return '';
+
+ return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
+ }
+
+
+ /**
+ * Add the header to a diff body
+ */
+ static function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
+ $header = "<table class='diff'>";
+ if( $diff ) { // Safari/Chrome show broken output if cols not used
+ $header .= "
+ <col class='diff-marker' />
+ <col class='diff-content' />
+ <col class='diff-marker' />
+ <col class='diff-content' />";
+ $colspan = 2;
+ $multiColspan = 4;
+ } else {
+ $colspan = 1;
+ $multiColspan = 2;
+ }
+ $header .= "
+ <tr valign='top'>
+ <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
+ <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
+ </tr>";
+
+ if ( $multi != '' ) {
+ $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
+ }
+ if ( $notice != '' ) {
+ $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>";
+ }
+
+ return $header . $diff . "</table>";
+ }
+
+ /**
+ * Use specified text instead of loading from the database
+ */
+ function setText( $oldText, $newText ) {
+ $this->mOldtext = $oldText;
+ $this->mNewtext = $newText;
+ $this->mTextLoaded = 2;
+ $this->mRevisionsLoaded = true;
+ }
+
+ /**
+ * Load revision metadata for the specified articles. If newid is 0, then compare
+ * the old article in oldid to the current article; if oldid is 0, then
+ * compare the current article to the immediately previous one (ignoring the
+ * value of newid).
+ *
+ * If oldid is false, leave the corresponding revision object set
+ * to false. This is impossible via ordinary user input, and is provided for
+ * API convenience.
+ */
+ function loadRevisionData() {
+ global $wgLang, $wgUser;
+ if ( $this->mRevisionsLoaded ) {
+ return true;
+ } else {
+ // Whether it succeeds or fails, we don't want to try again
+ $this->mRevisionsLoaded = true;
+ }
+
+ // Load the new revision object
+ $this->mNewRev = $this->mNewid
+ ? Revision::newFromId( $this->mNewid )
+ : Revision::newFromTitle( $this->mTitle );
+ if( !$this->mNewRev instanceof Revision )
+ return false;
+
+ // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
+ $this->mNewid = $this->mNewRev->getId();
+
+ // Check if page is editable
+ $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
+
+ // Set assorted variables
+ $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
+ $dateofrev = $wgLang->date( $this->mNewRev->getTimestamp(), true );
+ $timeofrev = $wgLang->time( $this->mNewRev->getTimestamp(), true );
+ $this->mNewPage = $this->mNewRev->getTitle();
+ if( $this->mNewRev->isCurrent() ) {
+ $newLink = $this->mNewPage->escapeLocalUrl( array(
+ 'oldid' => $this->mNewid
+ ) );
+ $this->mPagetitle = htmlspecialchars( wfMsg(
+ 'currentrev-asof',
+ $timestamp,
+ $dateofrev,
+ $timeofrev
+ ) );
+ $newEdit = $this->mNewPage->escapeLocalUrl( array(
+ 'action' => 'edit'
+ ) );
+
+ $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
+ $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
+ } else {
+ $newLink = $this->mNewPage->escapeLocalUrl( array(
+ 'oldid' => $this->mNewid
+ ) );
+ $newEdit = $this->mNewPage->escapeLocalUrl( array(
+ 'action' => 'edit',
+ 'oldid' => $this->mNewid
+ ) );
+ $this->mPagetitle = htmlspecialchars( wfMsg(
+ 'revisionasof',
+ $timestamp,
+ $dateofrev,
+ $timeofrev
+ ) );
+
+ $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>";
+ } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+ $this->mNewtitle = "<span class='history-deleted'>{$this->mNewtitle}</span>";
+ }
+
+ // Load the old revision object
+ $this->mOldRev = false;
+ if( $this->mOldid ) {
+ $this->mOldRev = Revision::newFromId( $this->mOldid );
+ } elseif ( $this->mOldid === 0 ) {
+ $rev = $this->mNewRev->getPrevious();
+ if( $rev ) {
+ $this->mOldid = $rev->getId();
+ $this->mOldRev = $rev;
+ } else {
+ // No previous revision; mark to show as first-version only.
+ $this->mOldid = false;
+ $this->mOldRev = false;
+ }
+ }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
+
+ if( is_null( $this->mOldRev ) ) {
+ return false;
+ }
+
+ if ( $this->mOldRev ) {
+ $this->mOldPage = $this->mOldRev->getTitle();
+
+ $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
+ $dateofrev = $wgLang->date( $this->mOldRev->getTimestamp(), true );
+ $timeofrev = $wgLang->time( $this->mOldRev->getTimestamp(), true );
+ $oldLink = $this->mOldPage->escapeLocalUrl( array(
+ 'oldid' => $this->mOldid
+ ) );
+ $oldEdit = $this->mOldPage->escapeLocalUrl( array(
+ 'action' => 'edit',
+ 'oldid' => $this->mOldid
+ ) );
+ $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t, $dateofrev, $timeofrev ) );
+
+ $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
+ . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
+ // Add an "undo" link
+ $newUndo = $this->mNewPage->escapeLocalUrl( array(
+ 'action' => 'edit',
+ 'undoafter' => $this->mOldid,
+ 'undo' => $this->mNewid
+ ) );
+ $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
+ $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
+ if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</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>';
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Load the text of the revisions, as well as revision data.
+ */
+ function loadText() {
+ if ( $this->mTextLoaded == 2 ) {
+ return true;
+ } else {
+ // Whether it succeeds or fails, we don't want to try again
+ $this->mTextLoaded = 2;
+ }
+
+ if ( !$this->loadRevisionData() ) {
+ return false;
+ }
+ if ( $this->mOldRev ) {
+ $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
+ if ( $this->mOldtext === false ) {
+ return false;
+ }
+ }
+ if ( $this->mNewRev ) {
+ $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
+ if ( $this->mNewtext === false ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Load the text of the new revision, not the old one
+ */
+ function loadNewText() {
+ if ( $this->mTextLoaded >= 1 ) {
+ return true;
+ } else {
+ $this->mTextLoaded = 1;
+ }
+ if ( !$this->loadRevisionData() ) {
+ return false;
+ }
+ $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
+ return true;
+ }
+}
diff --git a/includes/diff/HTMLDiff.php b/includes/diff/HTMLDiff.php
deleted file mode 100644
index df9f4eb8..00000000
--- a/includes/diff/HTMLDiff.php
+++ /dev/null
@@ -1,1009 +0,0 @@
-<?php
-
-/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- * or see http://www.gnu.org/
- *
- * @ingroup DifferenceEngine
- */
-
-/**
- * When detecting the last common parent of two nodes, all results are stored as
- * a LastCommonParentResult.
- */
-class LastCommonParentResult {
-
- // Parent
- public $parent;
-
- // Splitting
- public $splittingNeeded = false;
-
- // Depth
- public $lastCommonParentDepth = -1;
-
- // Index
- public $indexInLastCommonParent = -1;
-}
-
-class Modification{
-
- const NONE = 1;
- const REMOVED = 2;
- const ADDED = 4;
- const CHANGED = 8;
-
- public $type;
-
- public $id = -1;
-
- public $firstOfID = false;
-
- public $changes;
-
- function __construct($type) {
- $this->type = $type;
- }
-
- public static function typeToString($type) {
- switch($type) {
- case self::NONE: return 'none';
- case self::REMOVED: return 'removed';
- case self::ADDED: return 'added';
- case self::CHANGED: return 'changed';
- }
- }
-}
-
-class DomTreeBuilder {
-
- public $textNodes = array();
-
- public $bodyNode;
-
- private $currentParent;
-
- private $newWord = '';
-
- protected $bodyStarted = false;
-
- protected $bodyEnded = false;
-
- private $whiteSpaceBeforeThis = false;
-
- private $lastSibling;
-
- private $notInPre = true;
-
- function __construct() {
- $this->bodyNode = $this->currentParent = new BodyNode();
- $this->lastSibling = new DummyNode();
- }
-
- /**
- * Must be called manually
- */
- public function endDocument() {
- $this->endWord();
- HTMLDiffer::diffDebug( count($this->textNodes) . " text nodes in document.\n" );
- }
-
- public function startElement($parser, $name, /*array*/ $attributes) {
- if (strcasecmp($name, 'body') != 0) {
- HTMLDiffer::diffDebug( "Starting $name node.\n" );
- $this->endWord();
-
- $newNode = new TagNode($this->currentParent, $name, $attributes);
- $this->currentParent->children[] = $newNode;
- $this->currentParent = $newNode;
- $this->lastSibling = new DummyNode();
- if ($this->whiteSpaceBeforeThis && !in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
- $this->currentParent->whiteBefore = true;
- }
- $this->whiteSpaceBeforeThis = false;
- if(strcasecmp($name, 'pre') == 0) {
- $this->notInPre = false;
- }
- }
- }
-
- public function endElement($parser, $name) {
- if(strcasecmp($name, 'body') != 0) {
- HTMLDiffer::diffDebug( "Ending $name node.\n");
- if (0 == strcasecmp($name,'img')) {
- // Insert a dummy leaf for the image
- $img = new ImageNode($this->currentParent, $this->currentParent->attributes);
- $this->currentParent->children[] = $img;
- $img->whiteBefore = $this->whiteSpaceBeforeThis;
- $this->lastSibling = $img;
- $this->textNodes[] = $img;
- }
- $this->endWord();
- if (!in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
- $this->lastSibling = $this->currentParent;
- } else {
- $this->lastSibling = new DummyNode();
- }
- $this->currentParent = $this->currentParent->parent;
- $this->whiteSpaceBeforeThis = false;
- if (!$this->notInPre && strcasecmp($name, 'pre') == 0) {
- $this->notInPre = true;
- }
- } else {
- $this->endDocument();
- }
- }
-
- const regex = '/([\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1})/';
- const whitespace = '/^[\s]{1}$/';
- const delimiter = '/^[\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1}$/';
-
- public function characters($parser, $data) {
- $matches = preg_split(self::regex, $data, -1, PREG_SPLIT_DELIM_CAPTURE);
-
- foreach($matches as &$word) {
- if (preg_match(self::whitespace, $word) && $this->notInPre) {
- $this->endWord();
- $this->lastSibling->whiteAfter = true;
- $this->whiteSpaceBeforeThis = true;
- } else if (preg_match(self::delimiter, $word)) {
- $this->endWord();
- $textNode = new TextNode($this->currentParent, $word);
- $this->currentParent->children[] = $textNode;
- $textNode->whiteBefore = $this->whiteSpaceBeforeThis;
- $this->whiteSpaceBeforeThis = false;
- $this->lastSibling = $textNode;
- $this->textNodes[] = $textNode;
- } else {
- $this->newWord .= $word;
- }
- }
- }
-
- private function endWord() {
- if ($this->newWord !== '') {
- $node = new TextNode($this->currentParent, $this->newWord);
- $this->currentParent->children[] = $node;
- $node->whiteBefore = $this->whiteSpaceBeforeThis;
- $this->whiteSpaceBeforeThis = false;
- $this->lastSibling = $node;
- $this->textNodes[] = $node;
- $this->newWord = "";
- }
- }
-
- public function getDiffLines() {
- return array_map(array('TextNode','toDiffLine'), $this->textNodes);
- }
-}
-
-class TextNodeDiffer {
-
- private $textNodes;
- public $bodyNode;
-
- private $oldTextNodes;
- private $oldBodyNode;
-
- private $newID = 0;
-
- private $changedID = 0;
-
- private $changedIDUsed = false;
-
- // used to remove the whitespace between a red and green block
- private $whiteAfterLastChangedPart = false;
-
- private $deletedID = 0;
-
- function __construct(DomTreeBuilder $tree, DomTreeBuilder $oldTree) {
- $this->textNodes = $tree->textNodes;
- $this->bodyNode = $tree->bodyNode;
- $this->oldTextNodes = $oldTree->textNodes;
- $this->oldBodyNode = $oldTree->bodyNode;
- }
-
- public function markAsNew($start, $end) {
- if ($end <= $start) {
- return;
- }
-
- if ($this->whiteAfterLastChangedPart) {
- $this->textNodes[$start]->whiteBefore = false;
- }
-
- for ($i = $start; $i < $end; ++$i) {
- $mod = new Modification(Modification::ADDED);
- $mod->id = $this->newID;
- $this->textNodes[$i]->modification = $mod;
- }
- if ($start < $end) {
- $this->textNodes[$start]->modification->firstOfID = true;
- }
- ++$this->newID;
- }
-
- public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) {
- $i = $rightstart;
- $j = $leftstart;
-
- if ($this->changedIDUsed) {
- ++$this->changedID;
- $this->changedIDUsed = false;
- }
-
- $changes;
- while ($i < $rightend) {
- $acthis = new AncestorComparator($this->textNodes[$i]->getParentTree());
- $acother = new AncestorComparator($this->oldTextNodes[$j]->getParentTree());
- $result = $acthis->getResult($acother);
- unset($acthis, $acother);
-
- if ( $result ) {
- $mod = new Modification(Modification::CHANGED);
-
- if (!$this->changedIDUsed) {
- $mod->firstOfID = true;
- } else if (!is_null( $result ) && $result !== $this->changes) {
- ++$this->changedID;
- $mod->firstOfID = true;
- }
-
- $mod->changes = $result;
- $mod->id = $this->changedID;
-
- $this->textNodes[$i]->modification = $mod;
- $this->changes = $result;
- $this->changedIDUsed = true;
- } else if ($this->changedIDUsed) {
- ++$this->changedID;
- $this->changedIDUsed = false;
- }
- ++$i;
- ++$j;
- }
- }
-
- public function markAsDeleted($start, $end, $before) {
-
- if ($end <= $start) {
- return;
- }
-
- if ($before > 0 && $this->textNodes[$before - 1]->whiteAfter) {
- $this->whiteAfterLastChangedPart = true;
- } else {
- $this->whiteAfterLastChangedPart = false;
- }
-
- for ($i = $start; $i < $end; ++$i) {
- $mod = new Modification(Modification::REMOVED);
- $mod->id = $this->deletedID;
-
- // oldTextNodes is used here because we're going to move its deleted
- // elements to this tree!
- $this->oldTextNodes[$i]->modification = $mod;
- }
- $this->oldTextNodes[$start]->modification->firstOfID = true;
-
- $root = $this->oldTextNodes[$start]->getLastCommonParent($this->oldTextNodes[$end-1])->parent;
-
- $junk1 = $junk2 = null;
- $deletedNodes = $root->getMinimalDeletedSet($this->deletedID, $junk1, $junk2);
-
- HTMLDiffer::diffDebug( "Minimal set of deleted nodes of size " . count($deletedNodes) . "\n" );
-
- // Set prevLeaf to the leaf after which the old HTML needs to be
- // inserted
- if ($before > 0) {
- $prevLeaf = $this->textNodes[$before - 1];
- }
- // Set nextLeaf to the leaf before which the old HTML needs to be
- // inserted
- if ($before < count($this->textNodes)) {
- $nextLeaf = $this->textNodes[$before];
- }
-
- while (count($deletedNodes) > 0) {
- if (isset($prevLeaf)) {
- $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
- } else {
- $prevResult = new LastCommonParentResult();
- $prevResult->parent = $this->bodyNode;
- $prevResult->indexInLastCommonParent = -1;
- }
- if (isset($nextleaf)) {
- $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[count($deletedNodes) - 1]);
- } else {
- $nextResult = new LastCommonParentResult();
- $nextResult->parent = $this->bodyNode;
- $nextResult->indexInLastCommonParent = $this->bodyNode->getNbChildren();
- }
-
- if ($prevResult->lastCommonParentDepth == $nextResult->lastCommonParentDepth) {
- // We need some metric to choose which way to add-...
- if ($deletedNodes[0]->parent === $deletedNodes[count($deletedNodes) - 1]->parent
- && $prevResult->parent === $nextResult->parent) {
- // The difference is not in the parent
- $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
- } else {
- // The difference is in the parent, so compare them
- // now THIS is tricky
- $distancePrev = $deletedNodes[0]->parent->getMatchRatio($prevResult->parent);
- $distanceNext = $deletedNodes[count($deletedNodes) - 1]->parent->getMatchRatio($nextResult->parent);
-
- if ($distancePrev <= $distanceNext) {
- $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
- } else {
- $nextResult->lastCommonParentDepth = $nextResult->lastCommonParentDepth + 1;
- }
- }
-
- }
-
- if ($prevResult->lastCommonParentDepth > $nextResult->lastCommonParentDepth) {
- // Inserting at the front
- if ($prevResult->splittingNeeded) {
- $prevLeaf->parent->splitUntil($prevResult->parent, $prevLeaf, true);
- }
- $prevLeaf = $deletedNodes[0]->copyTree();
- unset($deletedNodes[0]);
- $deletedNodes = array_values($deletedNodes);
- $prevLeaf->setParent($prevResult->parent);
- $prevResult->parent->addChildAbsolute($prevLeaf,$prevResult->indexInLastCommonParent + 1);
- } else if ($prevResult->lastCommonParentDepth < $nextResult->lastCommonParentDepth) {
- // Inserting at the back
- if ($nextResult->splittingNeeded) {
- $splitOccured = $nextLeaf->parent->splitUntil($nextResult->parent, $nextLeaf, false);
- if ($splitOccured) {
- // The place where to insert is shifted one place to the
- // right
- $nextResult->indexInLastCommonParent = $nextResult->indexInLastCommonParent + 1;
- }
- }
- $nextLeaf = $deletedNodes[count(deletedNodes) - 1]->copyTree();
- unset($deletedNodes[count(deletedNodes) - 1]);
- $deletedNodes = array_values($deletedNodes);
- $nextLeaf->setParent($nextResult->parent);
- $nextResult->parent->addChildAbsolute($nextLeaf,$nextResult->indexInLastCommonParent);
- }
- }
- ++$this->deletedID;
- }
-
- public function expandWhiteSpace() {
- $this->bodyNode->expandWhiteSpace();
- }
-
- public function lengthNew(){
- return count($this->textNodes);
- }
-
- public function lengthOld(){
- return count($this->oldTextNodes);
- }
-}
-
-class HTMLDiffer {
-
- private $output;
- private static $debug = '';
-
- function __construct($output) {
- $this->output = $output;
- }
-
- function htmlDiff($from, $to) {
- wfProfileIn( __METHOD__ );
- // Create an XML parser
- $xml_parser = xml_parser_create('');
-
- $domfrom = new DomTreeBuilder();
-
- // Set the functions to handle opening and closing tags
- xml_set_element_handler($xml_parser, array($domfrom, "startElement"), array($domfrom, "endElement"));
-
- // Set the function to handle blocks of character data
- xml_set_character_data_handler($xml_parser, array($domfrom, "characters"));
-
- HTMLDiffer::diffDebug( "Parsing " . strlen($from) . " characters worth of HTML\n" );
- if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
- || !xml_parse($xml_parser, $from, false)
- || !xml_parse($xml_parser, '</body>', true)){
- $error = xml_error_string(xml_get_error_code($xml_parser));
- $line = xml_get_current_line_number($xml_parser);
- HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
- }
- xml_parser_free($xml_parser);
- unset($from);
-
- $xml_parser = xml_parser_create('');
-
- $domto = new DomTreeBuilder();
-
- // Set the functions to handle opening and closing tags
- xml_set_element_handler($xml_parser, array($domto, "startElement"), array($domto, "endElement"));
-
- // Set the function to handle blocks of character data
- xml_set_character_data_handler($xml_parser, array($domto, "characters"));
-
- HTMLDiffer::diffDebug( "Parsing " . strlen($to) . " characters worth of HTML\n" );
- if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
- || !xml_parse($xml_parser, $to, false)
- || !xml_parse($xml_parser, '</body>', true)){
- $error = xml_error_string(xml_get_error_code($xml_parser));
- $line = xml_get_current_line_number($xml_parser);
- HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
- }
- xml_parser_free($xml_parser);
- unset($to);
-
- $diffengine = new WikiDiff3();
- $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines()));
- unset($xml_parser, $diffengine);
-
- $domdiffer = new TextNodeDiffer($domto, $domfrom);
-
- $currentIndexLeft = 0;
- $currentIndexRight = 0;
- foreach ($differences as &$d) {
- if ($d->leftstart > $currentIndexLeft) {
- $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart,
- $currentIndexRight, $d->rightstart);
- }
- if ($d->leftlength > 0) {
- $domdiffer->markAsDeleted($d->leftstart, $d->leftend, $d->rightstart);
- }
- $domdiffer->markAsNew($d->rightstart, $d->rightend);
-
- $currentIndexLeft = $d->leftend;
- $currentIndexRight = $d->rightend;
- }
- $oldLength = $domdiffer->lengthOld();
- if ($currentIndexLeft < $oldLength) {
- $domdiffer->handlePossibleChangedPart($currentIndexLeft, $oldLength, $currentIndexRight, $domdiffer->lengthNew());
- }
- $domdiffer->expandWhiteSpace();
- $output = new HTMLOutput('htmldiff', $this->output);
- $output->parse($domdiffer->bodyNode);
- wfProfileOut( __METHOD__ );
- }
-
- private function preProcess(/*array*/ $differences) {
- $newRanges = array();
-
- $nbDifferences = count($differences);
- for ($i = 0; $i < $nbDifferences; ++$i) {
- $leftStart = $differences[$i]->leftstart;
- $leftEnd = $differences[$i]->leftend;
- $rightStart = $differences[$i]->rightstart;
- $rightEnd = $differences[$i]->rightend;
-
- $leftLength = $leftEnd - $leftStart;
- $rightLength = $rightEnd - $rightStart;
-
- while ($i + 1 < $nbDifferences && self::score($leftLength,
- $differences[$i + 1]->leftlength,
- $rightLength,
- $differences[$i + 1]->rightlength)
- > ($differences[$i + 1]->leftstart - $leftEnd)) {
- $leftEnd = $differences[$i + 1]->leftend;
- $rightEnd = $differences[$i + 1]->rightend;
- $leftLength = $leftEnd - $leftStart;
- $rightLength = $rightEnd - $rightStart;
- ++$i;
- }
- $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd);
- }
- return $newRanges;
- }
-
- /**
- * Heuristic to merge differences for readability.
- */
- public static function score($ll, $nll, $rl, $nrl) {
- if (($ll == 0 && $nll == 0)
- || ($rl == 0 && $nrl == 0)) {
- return 0;
- }
- $numbers = array($ll, $nll, $rl, $nrl);
- $d = 0;
- foreach ($numbers as &$number) {
- while ($number > 3) {
- $d += 3;
- $number -= 3;
- $number *= 0.5;
- }
- $d += $number;
-
- }
- return $d / (1.5 * count($numbers));
- }
-
- /**
- * Add to debug output
- * @param string $str Debug output
- */
- public static function diffDebug( $str ) {
- self :: $debug .= $str;
- }
-
- /**
- * Get debug output
- * @return string
- */
- public static function getDebugOutput() {
- return self :: $debug;
- }
-
-}
-
-class TextOnlyComparator {
-
- public $leafs = array();
-
- function _construct(TagNode $tree) {
- $this->addRecursive($tree);
- $this->leafs = array_map(array('TextNode','toDiffLine'), $this->leafs);
- }
-
- private function addRecursive(TagNode $tree) {
- foreach ($tree->children as &$child) {
- if ($child instanceof TagNode) {
- $this->addRecursive($child);
- } else if ($child instanceof TextNode) {
- $this->leafs[] = $node;
- }
- }
- }
-
- public function getMatchRatio(TextOnlyComparator $other) {
- $nbOthers = count($other->leafs);
- $nbThis = count($this->leafs);
- if($nbOthers == 0 || $nbThis == 0){
- return -log(0);
- }
-
- $diffengine = new WikiDiff3(25000, 1.35);
- $diffengine->diff($this->leafs, $other->leafs);
-
- $lcsLength = $diffengine->getLcsLength();
-
- $distanceThis = $nbThis-$lcsLength;
-
- return (2.0 - $lcsLength/$nbOthers - $lcsLength/$nbThis) / 2.0;
- }
-}
-
-/**
- * A comparator used when calculating the difference in ancestry of two Nodes.
- */
-class AncestorComparator {
-
- public $ancestors;
- public $ancestorsText;
-
- function __construct(/*array*/ $ancestors) {
- $this->ancestors = $ancestors;
- $this->ancestorsText = array_map(array('TagNode','toDiffLine'), $ancestors);
- }
-
- public $compareTxt = "";
-
- public function getResult(AncestorComparator $other) {
-
- $diffengine = new WikiDiff3(10000, 1.35);
- $differences = $diffengine->diff_range($other->ancestorsText,$this->ancestorsText);
-
- if (count($differences) == 0){
- return null;
- }
- $changeTxt = new ChangeTextGenerator($this, $other);
-
- return $changeTxt->getChanged($differences)->toString();;
- }
-}
-
-class ChangeTextGenerator {
-
- private $ancestorComparator;
- private $other;
-
- private $factory;
-
- function __construct(AncestorComparator $ancestorComparator, AncestorComparator $other) {
- $this->ancestorComparator = $ancestorComparator;
- $this->other = $other;
- $this->factory = new TagToStringFactory();
- }
-
- public function getChanged(/*array*/ $differences) {
- $txt = new ChangeText;
- $rootlistopened = false;
- if (count($differences) > 1) {
- $txt->addHtml('<ul class="changelist">');
- $rootlistopened = true;
- }
- $nbDifferences = count($differences);
- for ($j = 0; $j < $nbDifferences; ++$j) {
- $d = $differences[$j];
- $lvl1listopened = false;
- if ($rootlistopened) {
- $txt->addHtml('<li>');
- }
- if ($d->leftlength + $d->rightlength > 1) {
- $txt->addHtml('<ul class="changelist">');
- $lvl1listopened = true;
- }
- // left are the old ones
- for ($i = $d->leftstart; $i < $d->leftend; ++$i) {
- if ($lvl1listopened){
- $txt->addHtml('<li>');
- }
- // add a bullet for a old tag
- $this->addTagOld($txt, $this->other->ancestors[$i]);
- if ($lvl1listopened){
- $txt->addHtml('</li>');
- }
- }
- // right are the new ones
- for ($i = $d->rightstart; $i < $d->rightend; ++$i) {
- if ($lvl1listopened){
- $txt->addHtml('<li>');
- }
- // add a bullet for a new tag
- $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]);
-
- if ($lvl1listopened){
- $txt->addHtml('</li>');
- }
- }
- if ($lvl1listopened) {
- $txt->addHtml('</ul>');
- }
- if ($rootlistopened) {
- $txt->addHtml('</li>');
- }
- }
- if ($rootlistopened) {
- $txt->addHtml('</ul>');
- }
- return $txt;
- }
-
- private function addTagOld(ChangeText $txt, TagNode $ancestor) {
- $this->factory->create($ancestor)->getRemovedDescription($txt);
- }
-
- private function addTagNew(ChangeText $txt, TagNode $ancestor) {
- $this->factory->create($ancestor)->getAddedDescription($txt);
- }
-}
-
-class ChangeText {
-
- private $txt = "";
-
- public function addHtml($s) {
- $this->txt .= $s;
- }
-
- public function toString() {
- return $this->txt;
- }
-}
-
-class TagToStringFactory {
-
- private static $containerTags = array('html', 'body', 'p', 'blockquote',
- 'h1', 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li',
- 'table', 'tbody', 'tr', 'td', 'th', 'br', 'hr', 'code', 'dl',
- 'dt', 'dd', 'input', 'form', 'img', 'span', 'a');
-
- private static $styleTags = array('i', 'b', 'strong', 'em', 'font',
- 'big', 'del', 'tt', 'sub', 'sup', 'strike');
-
- const MOVED = 1;
- const STYLE = 2;
- const UNKNOWN = 4;
-
- public function create(TagNode $node) {
- $sem = $this->getChangeSemantic($node->qName);
- if (strcasecmp($node->qName,'a') == 0) {
- return new AnchorToString($node, $sem);
- }
- if (strcasecmp($node->qName,'img') == 0) {
- return new NoContentTagToString($node, $sem);
- }
- return new TagToString($node, $sem);
- }
-
- protected function getChangeSemantic($qname) {
- if (in_array(strtolower($qname),self::$containerTags)) {
- return self::MOVED;
- }
- if (in_array(strtolower($qname),self::$styleTags)) {
- return self::STYLE;
- }
- return self::UNKNOWN;
- }
-}
-
-class TagToString {
-
- protected $node;
-
- protected $sem;
-
- function __construct(TagNode $node, $sem) {
- $this->node = $node;
- $this->sem = $sem;
- }
-
- public function getRemovedDescription(ChangeText $txt) {
- $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
- if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
- $tagDescription = "&lt;" . $this->node->qName . "&gt;";
- }
- if ($this->sem == TagToStringFactory::MOVED) {
- $txt->addHtml( wfMsgExt( 'diff-movedoutof', 'parseinline', $tagDescription ) );
- } else if ($this->sem == TagToStringFactory::STYLE) {
- $txt->addHtml( wfMsgExt( 'diff-styleremoved' , 'parseinline', $tagDescription ) );
- } else {
- $txt->addHtml( wfMsgExt( 'diff-removed' , 'parseinline', $tagDescription ) );
- }
- $this->addAttributes($txt, $this->node->attributes);
- $txt->addHtml('.');
- }
-
- public function getAddedDescription(ChangeText $txt) {
- $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
- if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
- $tagDescription = "&lt;" . $this->node->qName . "&gt;";
- }
- if ($this->sem == TagToStringFactory::MOVED) {
- $txt->addHtml( wfMsgExt( 'diff-movedto' , 'parseinline', $tagDescription) );
- } else if ($this->sem == TagToStringFactory::STYLE) {
- $txt->addHtml( wfMsgExt( 'diff-styleadded', 'parseinline', $tagDescription ) );
- } else {
- $txt->addHtml( wfMsgExt( 'diff-added', 'parseinline', $tagDescription ) );
- }
- $this->addAttributes($txt, $this->node->attributes);
- $txt->addHtml('.');
- }
-
- protected function addAttributes(ChangeText $txt, array $attributes) {
- if (count($attributes) < 1) {
- return;
- }
- $firstOne = true;
- $nbAttributes_min_1 = count($attributes)-1;
- $keys = array_keys($attributes);
- for ($i=0;$i<$nbAttributes_min_1;$i++) {
- $key = $keys[$i];
- $attr = $attributes[$key];
- if($firstOne) {
- $firstOne = false;
- $txt->addHtml( wfMsgExt('diff-with', 'escapenoentities', $this->translateArgument($key), htmlspecialchars($attr) ) );
- continue;
- }
- $txt->addHtml( wfMsgExt( 'comma-separator', 'escapenoentities' ) .
- wfMsgExt( 'diff-with-additional', 'escapenoentities',
- $this->translateArgument( $key ), htmlspecialchars( $attr ) )
- );
- }
-
- if ($nbAttributes_min_1 > 0) {
- $txt->addHtml( wfMsgExt( 'diff-with-final', 'escapenoentities',
- $this->translateArgument($keys[$nbAttributes_min_1]),
- htmlspecialchars($attributes[$keys[$nbAttributes_min_1]]) ) );
- }
- }
-
- protected function translateArgument($name) {
- $translation = wfMsgExt('diff-' . $name, 'parseinline' );
- if ( wfEmptyMsg( 'diff-' . $name, $translation ) ) {
- $translation = "&lt;" . $name . "&gt;";;
- }
- return htmlspecialchars( $translation );
- }
-}
-
-class NoContentTagToString extends TagToString {
-
- function __construct(TagNode $node, $sem) {
- parent::__construct($node, $sem);
- }
-
- public function getAddedDescription(ChangeText $txt) {
- $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
- if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
- $tagDescription = "&lt;" . $this->node->qName . "&gt;";
- }
- $txt->addHtml( wfMsgExt('diff-changedto', 'parseinline', $tagDescription ) );
- $this->addAttributes($txt, $this->node->attributes);
- $txt->addHtml('.');
- }
-
- public function getRemovedDescription(ChangeText $txt) {
- $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
- if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
- $tagDescription = "&lt;" . $this->node->qName . "&gt;";
- }
- $txt->addHtml( wfMsgExt('diff-changedfrom', 'parseinline', $tagDescription ) );
- $this->addAttributes($txt, $this->node->attributes);
- $txt->addHtml('.');
- }
-}
-
-class AnchorToString extends TagToString {
-
- function __construct(TagNode $node, $sem) {
- parent::__construct($node, $sem);
- }
-
- protected function addAttributes(ChangeText $txt, array $attributes) {
- if (array_key_exists('href', $attributes)) {
- $txt->addHtml(' ' . wfMsgExt( 'diff-withdestination', 'parseinline', htmlspecialchars($attributes['href']) ) );
- unset($attributes['href']);
- }
- parent::addAttributes($txt, $attributes);
- }
-}
-
-/**
- * Takes a branch root and creates an HTML file for it.
- */
-class HTMLOutput{
-
- private $prefix;
- private $handler;
-
- function __construct($prefix, $handler) {
- $this->prefix = $prefix;
- $this->handler = $handler;
- }
-
- public function parse(TagNode $node) {
- $handler = &$this->handler;
-
- if (strcasecmp($node->qName, 'img') != 0 && strcasecmp($node->qName, 'body') != 0) {
- $handler->startElement($node->qName, $node->attributes);
- }
-
- $newStarted = false;
- $remStarted = false;
- $changeStarted = false;
- $changeTXT = '';
-
- foreach ($node->children as &$child) {
- if ($child instanceof TagNode) {
- if ($newStarted) {
- $handler->endElement('span');
- $newStarted = false;
- } else if ($changeStarted) {
- $handler->endElement('span');
- $changeStarted = false;
- } else if ($remStarted) {
- $handler->endElement('span');
- $remStarted = false;
- }
- $this->parse($child);
- } else if ($child instanceof TextNode) {
- $mod = $child->modification;
-
- if ($newStarted && ($mod->type != Modification::ADDED || $mod->firstOfID)) {
- $handler->endElement('span');
- $newStarted = false;
- } else if ($changeStarted && ($mod->type != Modification::CHANGED
- || $mod->changes != $changeTXT || $mod->firstOfID)) {
- $handler->endElement('span');
- $changeStarted = false;
- } else if ($remStarted && ($mod->type != Modification::REMOVED || $mod ->firstOfID)) {
- $handler->endElement('span');
- $remStarted = false;
- }
-
- // no else because a removed part can just be closed and a new
- // part can start
- if (!$newStarted && $mod->type == Modification::ADDED) {
- $attrs = array('class' => 'diff-html-added');
- if ($mod->firstOfID) {
- $attrs['id'] = "added-{$this->prefix}-{$mod->id}";
- }
- $handler->startElement('span', $attrs);
- $newStarted = true;
- } else if (!$changeStarted && $mod->type == Modification::CHANGED) {
- $attrs = array('class' => 'diff-html-changed');
- if ($mod->firstOfID) {
- $attrs['id'] = "changed-{$this->prefix}-{$mod->id}";
- }
- $handler->startElement('span', $attrs);
-
- //tooltip
- $handler->startElement('span', array('class' => 'tip'));
- $handler->html($mod->changes);
- $handler->endElement('span');
-
- $changeStarted = true;
- $changeTXT = $mod->changes;
- } else if (!$remStarted && $mod->type == Modification::REMOVED) {
- $attrs = array('class'=>'diff-html-removed');
- if ($mod->firstOfID) {
- $attrs['id'] = "removed-{$this->prefix}-{$mod->id}";
- }
- $handler->startElement('span', $attrs);
- $remStarted = true;
- }
-
- $chars = $child->text;
-
- if ($child instanceof ImageNode) {
- $this->writeImage($child);
- } else {
- $handler->characters($chars);
- }
- }
- }
-
- if ($newStarted) {
- $handler->endElement('span');
- $newStarted = false;
- } else if ($changeStarted) {
- $handler->endElement('span');
- $changeStarted = false;
- } else if ($remStarted) {
- $handler->endElement('span');
- $remStarted = false;
- }
-
- if (strcasecmp($node->qName, 'img') != 0
- && strcasecmp($node->qName, 'body') != 0) {
- $handler->endElement($node->qName);
- }
- }
-
- private function writeImage(ImageNode $imgNode) {
- $attrs = $imgNode->attributes;
- $this->handler->startElement('img', $attrs);
- $this->handler->endElement('img');
- }
-}
-
-class DelegatingContentHandler {
-
- private $delegate;
-
- function __construct($delegate) {
- $this->delegate = $delegate;
- }
-
- function startElement($qname, /*array*/ $arguments) {
- $this->delegate->addHtml(Xml::openElement($qname, $arguments));
- }
-
- function endElement($qname){
- $this->delegate->addHtml(Xml::closeElement($qname));
- }
-
- function characters($chars){
- $this->delegate->addHtml(htmlspecialchars($chars));
- }
-
- function html($html){
- $this->delegate->addHtml($html);
- }
-}
diff --git a/includes/diff/Nodes.php b/includes/diff/Nodes.php
deleted file mode 100644
index 1b1363d4..00000000
--- a/includes/diff/Nodes.php
+++ /dev/null
@@ -1,439 +0,0 @@
-<?php
-
-/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- * or see http://www.gnu.org/
- *
- */
-
-/**
- * Any element in the DOM tree of an HTML document.
- * @ingroup DifferenceEngine
- */
-class Node {
-
- public $parent;
-
- protected $parentTree;
-
- public $whiteBefore = false;
-
- public $whiteAfter = false;
-
- function __construct($parent) {
- $this->parent = $parent;
- }
-
- public function getParentTree() {
- if (!isset($this->parentTree)) {
- if (!is_null($this->parent)) {
- $this->parentTree = $this->parent->getParentTree();
- $this->parentTree[] = $this->parent;
- } else {
- $this->parentTree = array();
- }
- }
- return $this->parentTree;
- }
-
- public function getLastCommonParent(Node $other) {
- $result = new LastCommonParentResult();
-
- $myParents = $this->getParentTree();
- $otherParents = $other->getParentTree();
-
- $i = 1;
- $isSame = true;
- $nbMyParents = count($myParents);
- $nbOtherParents = count($otherParents);
- while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) {
- if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) {
- $isSame = false;
- } else {
- // After a while, the index i-1 must be the last common parent
- $i++;
- }
- }
-
- $result->lastCommonParentDepth = $i - 1;
- $result->parent = $myParents[$i - 1];
-
- if (!$isSame || $nbMyParents > $nbOtherParents) {
- // Not all tags matched, or all tags matched but
- // there are tags left in this tree
- $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]);
- $result->splittingNeeded = true;
- } else if ($nbMyParents <= $nbOtherParents) {
- $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this);
- }
- return $result;
- }
-
- public function setParent($parent) {
- $this->parent = $parent;
- unset($this->parentTree);
- }
-
- public function inPre() {
- $tree = $this->getParentTree();
- foreach ($tree as &$ancestor) {
- if ($ancestor->isPre()) {
- return true;
- }
- }
- return false;
- }
-}
-
-/**
- * Node that can contain other nodes. Represents an HTML tag.
- * @ingroup DifferenceEngine
- */
-class TagNode extends Node {
-
- public $children = array();
-
- public $qName;
-
- public $attributes = array();
-
- public $openingTag;
-
- function __construct($parent, $qName, /*array*/ $attributes) {
- parent::__construct($parent);
- $this->qName = strtolower($qName);
- foreach($attributes as $key => &$value){
- $this->attributes[strtolower($key)] = $value;
- }
- return $this->openingTag = Xml::openElement($this->qName, $this->attributes);
- }
-
- public function addChildAbsolute(Node $node, $index) {
- array_splice($this->children, $index, 0, array($node));
- }
-
- public function getIndexOf(Node $child) {
- // don't trust array_search with objects
- foreach ($this->children as $key => &$value){
- if ($value === $child) {
- return $key;
- }
- }
- return null;
- }
-
- public function getNbChildren() {
- return count($this->children);
- }
-
- public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
- $nodes = array();
-
- $allDeleted = false;
- $somethingDeleted = false;
- $hasNonDeletedDescendant = false;
-
- if (empty($this->children)) {
- return $nodes;
- }
-
- foreach ($this->children as &$child) {
- $allDeleted_local = false;
- $somethingDeleted_local = false;
- $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local);
- if ($somethingDeleted_local) {
- $nodes = array_merge($nodes, $childrenChildren);
- $somethingDeleted = true;
- }
- if (!$allDeleted_local) {
- $hasNonDeletedDescendant = true;
- }
- }
- if (!$hasNonDeletedDescendant) {
- $nodes = array($this);
- $allDeleted = true;
- }
- return $nodes;
- }
-
- public function splitUntil(TagNode $parent, Node $split, $includeLeft) {
- $splitOccured = false;
- if ($parent !== $this) {
- $part1 = new TagNode(null, $this->qName, $this->attributes);
- $part2 = new TagNode(null, $this->qName, $this->attributes);
- $part1->setParent($this->parent);
- $part2->setParent($this->parent);
-
- $onSplit = false;
- $pastSplit = false;
- foreach ($this->children as &$child)
- {
- if ($child === $split) {
- $onSplit = true;
- }
- if(!$pastSplit || ($onSplit && $includeLeft)) {
- $child->setParent($part1);
- $part1->children[] = $child;
- } else {
- $child->setParent($part2);
- $part2->children[] = $child;
- }
- if ($onSplit) {
- $onSplit = false;
- $pastSplit = true;
- }
- }
- $myindexinparent = $this->parent->getIndexOf($this);
- if (!empty($part1->children)) {
- $this->parent->addChildAbsolute($part1, $myindexinparent);
- }
- if (!empty($part2->children)) {
- $this->parent->addChildAbsolute($part2, $myindexinparent);
- }
- if (!empty($part1->children) && !empty($part2->children)) {
- $splitOccured = true;
- }
-
- $this->parent->removeChild($myindexinparent);
-
- if ($includeLeft) {
- $this->parent->splitUntil($parent, $part1, $includeLeft);
- } else {
- $this->parent->splitUntil($parent, $part2, $includeLeft);
- }
- }
- return $splitOccured;
-
- }
-
- private function removeChild($index) {
- unset($this->children[$index]);
- $this->children = array_values($this->children);
- }
-
- public static $blocks = array('html', 'body','p','blockquote', 'h1',
- 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table',
- 'tbody', 'tr', 'td', 'th', 'br');
-
- public function copyTree() {
- $newThis = new TagNode(null, $this->qName, $this->attributes);
- $newThis->whiteBefore = $this->whiteBefore;
- $newThis->whiteAfter = $this->whiteAfter;
- foreach ($this->children as &$child) {
- $newChild = $child->copyTree();
- $newChild->setParent($newThis);
- $newThis->children[] = $newChild;
- }
- return $newThis;
- }
-
- public function getMatchRatio(TagNode $other) {
- $txtComp = new TextOnlyComparator($other);
- return $txtComp->getMatchRatio(new TextOnlyComparator($this));
- }
-
- public function expandWhiteSpace() {
- $shift = 0;
- $spaceAdded = false;
-
- $nbOriginalChildren = $this->getNbChildren();
- for ($i = 0; $i < $nbOriginalChildren; ++$i) {
- $child = $this->children[$i + $shift];
-
- if ($child instanceof TagNode) {
- if (!$child->isPre()) {
- $child->expandWhiteSpace();
- }
- }
- if (!$spaceAdded && $child->whiteBefore) {
- $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild());
- $ws->setParent($this);
- $this->addChildAbsolute($ws,$i + ($shift++));
- }
- if ($child->whiteAfter) {
- $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild());
- $ws->setParent($this);
- $this->addChildAbsolute($ws,$i + 1 + ($shift++));
- $spaceAdded = true;
- } else {
- $spaceAdded = false;
- }
-
- }
- }
-
- public function getLeftMostChild() {
- if (empty($this->children)) {
- return $this;
- }
- return $this->children[0]->getLeftMostChild();
- }
-
- public function getRightMostChild() {
- if (empty($this->children)) {
- return $this;
- }
- return $this->children[$this->getNbChildren() - 1]->getRightMostChild();
- }
-
- public function isPre() {
- return 0 == strcasecmp($this->qName,'pre');
- }
-
- public static function toDiffLine(TagNode $node) {
- return $node->openingTag;
- }
-}
-
-/**
- * Represents a piece of text in the HTML file.
- * @ingroup DifferenceEngine
- */
-class TextNode extends Node {
-
- public $text;
-
- public $modification;
-
- function __construct($parent, $text) {
- parent::__construct($parent);
- $this->modification = new Modification(Modification::NONE);
- $this->text = $text;
- }
-
- public function copyTree() {
- $clone = clone $this;
- $clone->setParent(null);
- return $clone;
- }
-
- public function getLeftMostChild() {
- return $this;
- }
-
- public function getRightMostChild() {
- return $this;
- }
-
- public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
- if ($this->modification->type == Modification::REMOVED
- && $this->modification->id == $id){
- $somethingDeleted = true;
- $allDeleted = true;
- return array($this);
- }
- return array();
- }
-
- public function isSameText($other) {
- if (is_null($other) || ! $other instanceof TextNode) {
- return false;
- }
- return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text);
- }
-
- public static function toDiffLine(TextNode $node) {
- return str_replace('\n', ' ',$node->text);
- }
-}
-
-/**
- * @todo Document
- * @ingroup DifferenceEngine
- */
-class WhiteSpaceNode extends TextNode {
-
- function __construct($parent, $s, Node $like = null) {
- parent::__construct($parent, $s);
- if(!is_null($like) && $like instanceof TextNode) {
- $newModification = clone $like->modification;
- $newModification->firstOfID = false;
- $this->modification = $newModification;
- }
- }
-}
-
-/**
- * Represents the root of a HTML document.
- * @ingroup DifferenceEngine
- */
-class BodyNode extends TagNode {
-
- function __construct() {
- parent::__construct(null, 'body', array());
- }
-
- public function copyTree() {
- $newThis = new BodyNode();
- foreach ($this->children as &$child) {
- $newChild = $child->copyTree();
- $newChild->setParent($newThis);
- $newThis->children[] = $newChild;
- }
- return $newThis;
- }
-
- public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
- $nodes = array();
- foreach ($this->children as &$child) {
- $childrenChildren = $child->getMinimalDeletedSet($id,
- $allDeleted, $somethingDeleted);
- $nodes = array_merge($nodes, $childrenChildren);
- }
- return $nodes;
- }
-
-}
-
-/**
- * Represents an image in HTML. Even though images do not contain any text they
- * are independent visible objects on the page. They are logically a TextNode.
- * @ingroup DifferenceEngine
- */
-class ImageNode extends TextNode {
-
- public $attributes;
-
- function __construct(TagNode $parent, /*array*/ $attrs) {
- if(!array_key_exists('src', $attrs)) {
- HTMLDiffer::diffDebug( "Image without a source\n" );
- parent::__construct($parent, '<img></img>');
- }else{
- parent::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>');
- }
- $this->attributes = $attrs;
- }
-
- public function isSameText($other) {
- if (is_null($other) || ! $other instanceof ImageNode) {
- return false;
- }
- return $this->text === $other->text;
- }
-
-}
-
-/**
- * No-op node
- * @ingroup DifferenceEngine
- */
-class DummyNode extends Node {
-
- function __construct() {
- // no op
- }
-
-}
diff --git a/includes/extauth/Hardcoded.php b/includes/extauth/Hardcoded.php
new file mode 100644
index 00000000..a9a60bea
--- /dev/null
+++ b/includes/extauth/Hardcoded.php
@@ -0,0 +1,79 @@
+<?php
+
+# Copyright (C) 2009 Aryeh Gregor
+#
+# 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 class supports external authentication from a literal array dumped in
+ * LocalSettings.php. It's mostly useful for testing. Example configuration:
+ *
+ * $wgExternalAuthType = 'ExternalUser_Hardcoded';
+ * $wgExternalAuthConf = array(
+ * 'Bob Smith' => array(
+ * 'password' => 'literal string',
+ * 'emailaddress' => 'bob@example.com',
+ * ),
+ * );
+ *
+ * Multiple names may be provided. The keys of the inner arrays can be either
+ * 'password', or the name of any preference.
+ *
+ * @ingroup ExternalUser
+ */
+class ExternalUser_Hardcoded extends ExternalUser {
+ private $mName;
+
+ protected function initFromName( $name ) {
+ global $wgExternalAuthConf;
+
+ if ( isset( $wgExternalAuthConf[$name] ) ) {
+ $this->mName = $name;
+ return true;
+ }
+ return false;
+ }
+
+ protected function initFromId( $id ) {
+ return $this->initFromName( $id );
+ }
+
+ public function getId() {
+ return $this->mName;
+ }
+
+ public function getName() {
+ return $this->mName;
+ }
+
+ public function authenticate( $password ) {
+ global $wgExternalAuthConf;
+
+ return isset( $wgExternalAuthConf[$this->mName]['password'] )
+ && $wgExternalAuthConf[$this->mName]['password'] == $password;
+ }
+
+ public function getPref( $pref ) {
+ global $wgExternalAuthConf;
+
+ if ( isset( $wgExternalAuthConf[$this->mName][$pref] ) ) {
+ return $wgExternalAuthConf[$this->mName][$pref];
+ }
+ return null;
+ }
+
+ # TODO: Implement setPref() via regex on LocalSettings. (Just kidding.)
+}
diff --git a/includes/extauth/MediaWiki.php b/includes/extauth/MediaWiki.php
new file mode 100644
index 00000000..7d6a3c71
--- /dev/null
+++ b/includes/extauth/MediaWiki.php
@@ -0,0 +1,141 @@
+<?php
+
+# Copyright (C) 2009 Aryeh Gregor
+#
+# 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 class supports authentication against an external MediaWiki database,
+ * probably any version back to 1.5 or something. Example configuration:
+ *
+ * $wgExternalAuthType = 'ExternalUser_MediaWiki';
+ * $wgExternalAuthConf = array(
+ * 'DBtype' => 'mysql',
+ * 'DBserver' => 'localhost',
+ * 'DBname' => 'wikidb',
+ * 'DBuser' => 'quasit',
+ * 'DBpassword' => 'a5Cr:yf9u-6[{`g',
+ * 'DBprefix' => '',
+ * );
+ *
+ * All fields must be present. These mean the same things as $wgDBtype,
+ * $wgDBserver, etc. This implementation is quite crude; it could easily
+ * support multiple database servers, for instance, and memcached, and it
+ * probably has bugs. Kind of hard to reuse code when things might rely on who
+ * knows what configuration globals.
+ *
+ * If either wiki uses the UserComparePasswords hook, password authentication
+ * might fail unexpectedly unless they both do the exact same validation.
+ * There may be other corner cases like this where this will fail, but it
+ * should be unlikely.
+ *
+ * @ingroup ExternalUser
+ */
+class ExternalUser_MediaWiki extends ExternalUser {
+ private $mRow, $mDb;
+
+ protected function initFromName( $name ) {
+ # We might not need the 'usable' bit, but let's be safe. Theoretically
+ # this might return wrong results for old versions, but it's probably
+ # good enough.
+ $name = User::getCanonicalName( $name, 'usable' );
+
+ if ( !is_string( $name ) ) {
+ return false;
+ }
+
+ return $this->initFromCond( array( 'user_name' => $name ) );
+ }
+
+ protected function initFromId( $id ) {
+ return $this->initFromCond( array( 'user_id' => $id ) );
+ }
+
+ private function initFromCond( $cond ) {
+ global $wgExternalAuthConf;
+
+ $class = 'Database' . $wgExternalAuthConf['DBtype'];
+ $this->mDb = new $class(
+ $wgExternalAuthConf['DBserver'],
+ $wgExternalAuthConf['DBuser'],
+ $wgExternalAuthConf['DBpassword'],
+ $wgExternalAuthConf['DBname'],
+ false,
+ 0,
+ $wgExternalAuthConf['DBprefix']
+ );
+
+ $row = $this->mDb->selectRow(
+ 'user',
+ array(
+ 'user_name', 'user_id', 'user_password', 'user_email',
+ 'user_email_authenticated'
+ ),
+ $cond,
+ __METHOD__
+ );
+ if ( !$row ) {
+ return false;
+ }
+ $this->mRow = $row;
+
+ return true;
+ }
+
+ # TODO: Implement initFromCookie().
+
+ public function getId() {
+ return $this->mRow->user_id;
+ }
+
+ public function getName() {
+ return $this->mRow->user_name;
+ }
+
+ public function authenticate( $password ) {
+ # This might be wrong if anyone actually uses the UserComparePasswords hook
+ # (on either end), so don't use this if you those are incompatible.
+ return User::comparePasswords( $this->mRow->user_password, $password,
+ $this->mRow->user_id );
+ }
+
+ public function getPref( $pref ) {
+ # FIXME: Return other prefs too. Lots of global-riddled code that does
+ # this normally.
+ if ( $pref === 'emailaddress'
+ && $this->row->user_email_authenticated !== null ) {
+ return $this->mRow->user_email;
+ }
+ return null;
+ }
+
+ public function getGroups() {
+ # FIXME: Untested.
+ $groups = array();
+ $res = $this->mDb->select(
+ 'user_groups',
+ 'ug_group',
+ array( 'ug_user' => $this->mRow->user_id ),
+ __METHOD__
+ );
+ foreach ( $res as $row ) {
+ $groups[] = $row->ug_group;
+ }
+ return $groups;
+ }
+
+ # TODO: Implement setPref().
+}
diff --git a/includes/extauth/vB.php b/includes/extauth/vB.php
new file mode 100644
index 00000000..23523665
--- /dev/null
+++ b/includes/extauth/vB.php
@@ -0,0 +1,140 @@
+<?php
+
+# Copyright (C) 2009 Aryeh Gregor
+#
+# 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 class supports the proprietary vBulletin forum system
+ * <http://www.vbulletin.com>, versions 3.5 and up. It calls no functions or
+ * code, only reads from the database. Example lines to put in
+ * LocalSettings.php:
+ *
+ * $wgExternalAuthType = 'ExternalUser_vB';
+ * $wgExternalAuthConf = array(
+ * 'server' => 'localhost',
+ * 'username' => 'forum',
+ * 'password' => 'udE,jSqDJ<""p=fI.K9',
+ * 'dbname' => 'forum',
+ * 'tableprefix' => '',
+ * 'cookieprefix' => 'bb'
+ * );
+ *
+ * @ingroup ExternalUser
+ */
+class ExternalUser_vB extends ExternalUser {
+ private $mDb, $mRow;
+
+ protected function initFromName( $name ) {
+ return $this->initFromCond( array( 'username' => $name ) );
+ }
+
+ protected function initFromId( $id ) {
+ return $this->initFromCond( array( 'userid' => $id ) );
+ }
+
+ protected function initFromCookie() {
+ # Try using the session table. It will only have a row if the user has
+ # an active session, so it might not always work, but it's a lot easier
+ # than trying to convince PHP to give us vB's $_SESSION.
+ global $wgExternalAuthConf;
+ if ( !isset( $wgExternalAuthConf['cookieprefix'] ) ) {
+ $prefix = 'bb';
+ } else {
+ $prefix = $wgExternalAuthConf['cookieprefix'];
+ }
+ if ( !isset( $_COOKIE["{$prefix}sessionhash"] ) ) {
+ return false;
+ }
+
+ $db = $this->getDb();
+
+ $row = $db->selectRow(
+ array( 'session', 'user' ),
+ $this->getFields(),
+ array(
+ 'session.userid = user.userid',
+ 'sessionhash' => $_COOKIE["{$prefix}sessionhash"]
+ ),
+ __METHOD__
+ );
+ if ( !$row ) {
+ return false;
+ }
+ $this->mRow = $row;
+
+ return true;
+ }
+
+ private function initFromCond( $cond ) {
+ $db = $this->getDb();
+
+ $row = $db->selectRow(
+ 'user',
+ $this->getFields(),
+ $cond,
+ __METHOD__
+ );
+ if ( !$row ) {
+ return false;
+ }
+ $this->mRow = $row;
+
+ return true;
+ }
+
+ private function getDb() {
+ global $wgExternalAuthConf;
+ return new Database(
+ $wgExternalAuthConf['server'],
+ $wgExternalAuthConf['username'],
+ $wgExternalAuthConf['password'],
+ $wgExternalAuthConf['dbname'],
+ false, 0,
+ $wgExternalAuthConf['tableprefix']
+ );
+ }
+
+ private function getFields() {
+ return array( 'user.userid', 'username', 'password', 'salt', 'email',
+ 'usergroupid', 'membergroupids' );
+ }
+
+ public function getId() { return $this->mRow->userid; }
+ public function getName() { return $this->mRow->username; }
+
+ public function authenticate( $password ) {
+ # vBulletin seemingly strips whitespace from passwords
+ $password = trim( $password );
+ return $this->mRow->password == md5( md5( $password )
+ . $this->mRow->salt );
+ }
+
+ public function getPref( $pref ) {
+ if ( $pref == 'emailaddress' && $this->mRow->email ) {
+ # TODO: only return if validated?
+ return $this->mRow->email;
+ }
+ return null;
+ }
+
+ public function getGroups() {
+ $groups = array( $this->mRow->usergroupid );
+ $groups = array_merge( $groups, explode( ',', $this->mRow->membergroupids ) );
+ $groups = array_unique( $groups );
+ return $groups;
+ }
+}
diff --git a/includes/filerepo/ArchivedFile.php b/includes/filerepo/ArchivedFile.php
index 68c93b8f..ffc06303 100644
--- a/includes/filerepo/ArchivedFile.php
+++ b/includes/filerepo/ArchivedFile.php
@@ -33,7 +33,7 @@ class ArchivedFile
$this->id = -1;
$this->title = false;
$this->name = false;
- $this->group = '';
+ $this->group = 'deleted'; // needed for direct use of constructor
$this->key = '';
$this->size = 0;
$this->bits = 0;
@@ -45,47 +45,48 @@ class ArchivedFile
$this->description = '';
$this->user = 0;
$this->user_text = '';
- $this->timestamp = NULL;
+ $this->timestamp = null;
$this->deleted = 0;
$this->dataLoaded = false;
-
+ $this->exists = false;
+
if( is_object($title) ) {
$this->title = $title;
$this->name = $title->getDBkey();
}
-
+
if ($id)
$this->id = $id;
-
+
if ($key)
$this->key = $key;
-
+
if (!$id && !$key && !is_object($title))
throw new MWException( "No specifications provided to ArchivedFile constructor." );
}
/**
* Loads a file object from the filearchive table
- * @return ResultWrapper
+ * @return true on success or null
*/
public function load() {
if ( $this->dataLoaded ) {
return true;
}
$conds = array();
-
+
if( $this->id > 0 )
$conds['fa_id'] = $this->id;
if( $this->key ) {
- $conds['fa_storage_group'] = $this->group;
+ $conds['fa_storage_group'] = $this->group;
$conds['fa_storage_key'] = $this->key;
}
if( $this->title )
$conds['fa_name'] = $this->title->getDBkey();
-
+
if( !count($conds))
throw new MWException( "No specific information for retrieving archived file" );
-
+
if( !$this->title || $this->title->getNamespace() == NS_FILE ) {
$dbr = wfGetDB( DB_SLAVE );
$res = $dbr->select( 'filearchive',
@@ -142,13 +143,14 @@ class ArchivedFile
return;
}
$this->dataLoaded = true;
+ $this->exists = true;
return true;
}
/**
* Loads a file object from the filearchive table
- * @return ResultWrapper
+ * @return ArchivedFile
*/
public static function newFromRow( $row ) {
$file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
@@ -176,7 +178,6 @@ class ArchivedFile
/**
* Return the associated title object
- * @public
*/
public function getTitle() {
return $this->title;
@@ -194,6 +195,11 @@ class ArchivedFile
return $this->id;
}
+ public function exists() {
+ $this->load();
+ return $this->exists;
+ }
+
/**
* Return the FileStore key
*/
@@ -203,6 +209,13 @@ class ArchivedFile
}
/**
+ * Return the FileStore key (overriding base File class)
+ */
+ public function getStorageKey() {
+ return $this->getKey();
+ }
+
+ /**
* Return the FileStore storage group
*/
public function getGroup() {
@@ -235,7 +248,6 @@ class ArchivedFile
/**
* Return the size of the image file, in bytes
- * @public
*/
public function getSize() {
$this->load();
@@ -244,7 +256,6 @@ class ArchivedFile
/**
* Return the bits of the image file, in bytes
- * @public
*/
public function getBits() {
$this->load();
@@ -337,30 +348,33 @@ class ArchivedFile
}
/**
- * int $field one of DELETED_* bitfield constants
+ * Returns the deletion bitfield
+ * @return int
+ */
+ public function getVisibility() {
+ $this->load();
+ return $this->deleted;
+ }
+
+ /**
* for file or revision rows
+ *
+ * @param $field Integer: one of DELETED_* bitfield constants
* @return bool
*/
public function isDeleted( $field ) {
+ $this->load();
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 $field Integer
* @return bool
*/
public function userCan( $field ) {
- if( ($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->deleted\n" );
- return $wgUser->isAllowed( $permission );
- } else {
- return true;
- }
+ $this->load();
+ return Revision::userCanBitfield( $this->deleted, $field );
}
}
diff --git a/includes/filerepo/FSRepo.php b/includes/filerepo/FSRepo.php
index d561e61b..0dd9d0f7 100644
--- a/includes/filerepo/FSRepo.php
+++ b/includes/filerepo/FSRepo.php
@@ -6,7 +6,7 @@
* @ingroup FileRepo
*/
class FSRepo extends FileRepo {
- var $directory, $deletedDir, $url, $deletedHashLevels;
+ var $directory, $deletedDir, $deletedHashLevels, $fileMode;
var $fileFactory = array( 'UnregisteredLocalFile', 'newFromTitle' );
var $oldFileFactory = false;
var $pathDisclosureProtection = 'simple';
@@ -23,6 +23,17 @@ class FSRepo extends FileRepo {
$this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
$info['deletedHashLevels'] : $this->hashLevels;
$this->deletedDir = isset( $info['deletedDir'] ) ? $info['deletedDir'] : false;
+ $this->fileMode = isset( $info['fileMode'] ) ? $info['fileMode'] : 0644;
+ if ( isset( $info['thumbDir'] ) ) {
+ $this->thumbDir = $info['thumbDir'];
+ } else {
+ $this->thumbDir = "{$this->directory}/thumb";
+ }
+ if ( isset( $info['thumbUrl'] ) ) {
+ $this->thumbUrl = $info['thumbUrl'];
+ } else {
+ $this->thumbUrl = "{$this->url}/thumb";
+ }
}
/**
@@ -57,13 +68,15 @@ class FSRepo extends FileRepo {
return "{$this->directory}/temp";
case 'deleted':
return $this->deletedDir;
+ case 'thumb':
+ return $this->thumbDir;
default:
return false;
}
}
/**
- * Get the URL corresponding to one of the three basic zones
+ * @see FileRepo::getZoneUrl()
*/
function getZoneUrl( $zone ) {
switch ( $zone ) {
@@ -72,9 +85,11 @@ class FSRepo extends FileRepo {
case 'temp':
return "{$this->url}/temp";
case 'deleted':
- return false; // no public URL
+ return parent::getZoneUrl( $zone ); // no public URL
+ case 'thumb':
+ return $this->thumbUrl;
default:
- return false;
+ return parent::getZoneUrl( $zone );
}
}
@@ -203,7 +218,7 @@ class FSRepo extends FileRepo {
}
}
if ( $good ) {
- chmod( $dstPath, 0644 );
+ $this->chmod( $dstPath );
$status->successCount++;
} else {
$status->failCount++;
@@ -212,6 +227,70 @@ class FSRepo extends FileRepo {
return $status;
}
+ function append( $srcPath, $toAppendPath, $flags = 0 ) {
+ $status = $this->newGood();
+
+ // Resolve the virtual URL
+ if ( self::isVirtualUrl( $srcPath ) ) {
+ $srcPath = $this->resolveVirtualUrl( $srcPath );
+ }
+ // Make sure the files are there
+ if ( !is_file( $srcPath ) )
+ $status->fatal( 'filenotfound', $srcPath );
+
+ if ( !is_file( $toAppendPath ) )
+ $status->fatal( 'filenotfound', $toAppendPath );
+
+ if ( !$status->isOk() ) return $status;
+
+ // Do the append
+ $chunk = file_get_contents( $toAppendPath );
+ if( $chunk === false ) {
+ $status->fatal( 'fileappenderrorread', $toAppendPath );
+ }
+
+ if( $status->isOk() ) {
+ if ( file_put_contents( $srcPath, $chunk, FILE_APPEND ) ) {
+ $status->value = $srcPath;
+ } else {
+ $status->fatal( 'fileappenderror', $toAppendPath, $srcPath);
+ }
+ }
+
+ if ( $flags & self::DELETE_SOURCE ) {
+ unlink( $toAppendPath );
+ }
+
+ return $status;
+ }
+
+ /**
+ * Checks existence of specified array of files.
+ *
+ * @param array $files URLs of files to check
+ * @param integer $flags Bitwise combination of the following flags:
+ * self::FILES_ONLY Mark file as existing only if it is a file (not directory)
+ * @return Either array of files and existence flags, or false
+ */
+ function fileExistsBatch( $files, $flags = 0 ) {
+ if ( !file_exists( $this->directory ) || !is_readable( $this->directory ) ) {
+ return false;
+ }
+ $result = array();
+ foreach ( $files as $key => $file ) {
+ if ( self::isVirtualUrl( $file ) ) {
+ $file = $this->resolveVirtualUrl( $file );
+ }
+ if( $flags & self::FILES_ONLY ) {
+ $result[$key] = is_file( $file );
+ } else {
+ $result[$key] = file_exists( $file );
+ }
+ }
+
+ return $result;
+ }
+
/**
* Take all available measures to prevent web accessibility of new deleted
* directories, in case the user has not configured offline storage
@@ -362,7 +441,7 @@ class FSRepo extends FileRepo {
$status->successCount++;
wfDebug(__METHOD__.": wrote tempfile $srcPath to $dstPath\n");
// Thread-safe override for umask
- chmod( $dstPath, 0644 );
+ $this->chmod( $dstPath );
} else {
$status->failCount++;
}
@@ -439,7 +518,7 @@ class FSRepo extends FileRepo {
$status->error( 'filerenameerror', $srcPath, $archivePath );
$good = false;
} else {
- @chmod( $archivePath, 0644 );
+ $this->chmod( $archivePath );
}
}
if ( $good ) {
@@ -534,4 +613,14 @@ class FSRepo extends FileRepo {
return strtr( $param, $this->simpleCleanPairs );
}
+ /**
+ * Chmod a file, supressing the warnings.
+ * @param String $path The path to change
+ */
+ protected function chmod( $path ) {
+ wfSuppressWarnings();
+ chmod( $path, $this->fileMode );
+ wfRestoreWarnings();
+ }
+
}
diff --git a/includes/filerepo/File.php b/includes/filerepo/File.php
index 523a1c09..d79a1661 100644
--- a/includes/filerepo/File.php
+++ b/includes/filerepo/File.php
@@ -529,7 +529,7 @@ abstract class File {
* @return MediaTransformOutput
*/
function transform( $params, $flags = 0 ) {
- global $wgUseSquid, $wgIgnoreImageErrors;
+ global $wgUseSquid, $wgIgnoreImageErrors, $wgThumbnailEpoch, $wgServer;
wfProfileIn( __METHOD__ );
do {
@@ -539,6 +539,12 @@ abstract class File {
break;
}
+ // Get the descriptionUrl to embed it as comment into the thumbnail. Bug 19791.
+ $descriptionUrl = $this->getDescriptionUrl();
+ if ( $descriptionUrl ) {
+ $params['descriptionUrl'] = $wgServer . $descriptionUrl;
+ }
+
$script = $this->getTransformScript();
if ( $script && !($flags & self::RENDER_NOW) ) {
// Use a script to transform on client request, if possible
@@ -561,9 +567,14 @@ abstract class File {
wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
$this->migrateThumbFile( $thumbName );
- if ( file_exists( $thumbPath ) ) {
- $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
- break;
+ if ( file_exists( $thumbPath )) {
+ $thumbTime = filemtime( $thumbPath );
+ if ( $thumbTime !== FALSE &&
+ gmdate( 'YmdHis', $thumbTime ) >= $wgThumbnailEpoch ) {
+
+ $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
+ break;
+ }
}
$thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
@@ -746,15 +757,6 @@ abstract class File {
return $path;
}
- /** Get relative path for a thumbnail file */
- function getThumbRel( $suffix = false ) {
- $path = 'thumb/' . $this->getRel();
- if ( $suffix !== false ) {
- $path .= '/' . $suffix;
- }
- return $path;
- }
-
/** Get the path of the archive directory, or a particular file if $suffix is specified */
function getArchivePath( $suffix = false ) {
return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix );
@@ -762,7 +764,11 @@ abstract class File {
/** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
function getThumbPath( $suffix = false ) {
- return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix );
+ $path = $this->repo->getZonePath('thumb') . '/' . $this->getRel();
+ if ( $suffix !== false ) {
+ $path .= '/' . $suffix;
+ }
+ return $path;
}
/** Get the URL of the archive directory, or a particular file if $suffix is specified */
@@ -778,7 +784,7 @@ abstract class File {
/** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */
function getThumbUrl( $suffix = false ) {
- $path = $this->repo->getZoneUrl('public') . '/thumb/' . $this->getUrlRel();
+ $path = $this->repo->getZoneUrl('thumb') . '/' . $this->getUrlRel();
if ( $suffix !== false ) {
$path .= '/' . rawurlencode( $suffix );
}
@@ -798,7 +804,7 @@ abstract class File {
/** Get the virtual URL for a thumbnail file or directory */
function getThumbVirtualUrl( $suffix = false ) {
- $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getUrlRel();
+ $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel();
if ( $suffix !== false ) {
$path .= '/' . rawurlencode( $suffix );
}
@@ -943,6 +949,14 @@ abstract class File {
function isDeleted( $field ) {
return false;
}
+
+ /**
+ * Return the deletion bitfield
+ * STUB
+ */
+ function getVisibility() {
+ return 0;
+ }
/**
* Was this file ever deleted from the wiki?
@@ -1007,8 +1021,9 @@ abstract class File {
}
/**
- * Returns 'true' if this image is a multipage document, e.g. a DJVU
- * document.
+ * Returns 'true' if this file is a type which supports multiple pages,
+ * e.g. DJVU or PDF. Note that this may be true even if the file in
+ * question only has a single page.
*
* @return Bool
*/
@@ -1069,15 +1084,15 @@ abstract class File {
* Get the HTML text of the description page, if available
*/
function getDescriptionText() {
- global $wgMemc, $wgContLang;
+ global $wgMemc, $wgLang;
if ( !$this->repo->fetchDescription ) {
return false;
}
- $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
+ $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgLang->getCode() );
if ( $renderUrl ) {
if ( $this->repo->descriptionCacheExpiry > 0 ) {
wfDebug("Attempting to get the description from cache...");
- $key = wfMemcKey( 'RemoteFileDescription', 'url', $wgContLang->getCode(),
+ $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', $wgLang->getCode(),
$this->getName() );
$obj = $wgMemc->get($key);
if ($obj) {
@@ -1125,6 +1140,19 @@ abstract class File {
}
/**
+ * Get the deletion archive key, <sha1>.<ext>
+ */
+ function getStorageKey() {
+ $hash = $this->getSha1();
+ if ( !$hash ) {
+ return false;
+ }
+ $ext = $this->getExtension();
+ $dotExt = $ext === '' ? '' : ".$ext";
+ return $hash . $dotExt;
+ }
+
+ /**
* Determine if the current user is allowed to view a particular
* field of this file, if it's marked as deleted.
* STUB
@@ -1173,7 +1201,7 @@ abstract class File {
wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
} else {
- $info['mime'] = NULL;
+ $info['mime'] = null;
$info['media_type'] = MEDIATYPE_UNKNOWN;
$info['metadata'] = '';
$info['sha1'] = '';
@@ -1259,6 +1287,10 @@ abstract class File {
function redirectedFrom( $from ) {
$this->redirected = $from;
}
+
+ function isMissing() {
+ return false;
+ }
}
/**
* Aliases for backwards compatibility with 1.6
diff --git a/includes/filerepo/FileCache.php b/includes/filerepo/FileCache.php
deleted file mode 100644
index 7840d1a3..00000000
--- a/includes/filerepo/FileCache.php
+++ /dev/null
@@ -1,156 +0,0 @@
-<?php
-/**
- * Cache of file objects, wrapping some RepoGroup functions to avoid redundant
- * queries. Loosely inspired by the LinkCache / LinkBatch classes for titles.
- *
- * ISSUE: Merge with RepoGroup?
- *
- * @ingroup FileRepo
- */
-class FileCache {
- var $repoGroup;
- var $cache = array(), $notFound = array();
-
- protected static $instance;
-
- /**
- * Get a FileCache instance. Typically, only one instance of FileCache
- * is needed in a MediaWiki invocation.
- */
- static function singleton() {
- if ( self::$instance ) {
- return self::$instance;
- }
- self::$instance = new FileCache( RepoGroup::singleton() );
- return self::$instance;
- }
-
- /**
- * Destroy the singleton instance, so that a new one will be created next
- * time singleton() is called.
- */
- static function destroySingleton() {
- self::$instance = null;
- }
-
- /**
- * Set the singleton instance to a given object
- */
- static function setSingleton( $instance ) {
- self::$instance = $instance;
- }
-
- /**
- * Construct a group of file repositories.
- * @param RepoGroup $repoGroup
- */
- function __construct( $repoGroup ) {
- $this->repoGroup = $repoGroup;
- }
-
-
- /**
- * Add some files to the cache. This is a fairly low-level function,
- * which most users should not need to call. Note that any existing
- * entries for the same keys will not be replaced. Call clearFiles()
- * first if you need that.
- * @param array $files array of File objects, indexed by DB key
- */
- function addFiles( $files ) {
- wfDebug( "FileCache adding ".count( $files )." files\n" );
- $this->cache += $files;
- }
-
- /**
- * Remove some files from the cache, so that their existence will be
- * rechecked. This is a fairly low-level function, which most users
- * should not need to call.
- * @param array $remove array indexed by DB keys to remove (the values are ignored)
- */
- function clearFiles( $remove ) {
- wfDebug( "FileCache clearing data for ".count( $remove )." files\n" );
- $this->cache = array_diff_keys( $this->cache, $remove );
- $this->notFound = array_diff_keys( $this->notFound, $remove );
- }
-
- /**
- * Mark some DB keys as nonexistent. This is a fairly low-level
- * function, which most users should not need to call.
- * @param array $dbkeys array of DB keys
- */
- function markNotFound( $dbkeys ) {
- wfDebug( "FileCache marking ".count( $dbkeys )." files as not found\n" );
- $this->notFound += array_fill_keys( $dbkeys, true );
- }
-
-
- /**
- * Search the cache for a file.
- * @param mixed $title Title object or string
- * @return File object or false if it is not found
- * @todo Implement searching for old file versions(?)
- */
- function findFile( $title ) {
- if( !( $title instanceof Title ) ) {
- $title = Title::makeTitleSafe( NS_FILE, $title );
- }
- if( !$title ) {
- return false; // invalid title?
- }
-
- $dbkey = $title->getDBkey();
- if( array_key_exists( $dbkey, $this->cache ) ) {
- wfDebug( "FileCache HIT for $dbkey\n" );
- return $this->cache[$dbkey];
- }
- if( array_key_exists( $dbkey, $this->notFound ) ) {
- wfDebug( "FileCache negative HIT for $dbkey\n" );
- return false;
- }
-
- // Not in cache, fall back to a direct query
- $file = $this->repoGroup->findFile( $title );
- if( $file ) {
- wfDebug( "FileCache MISS for $dbkey\n" );
- $this->cache[$dbkey] = $file;
- } else {
- wfDebug( "FileCache negative MISS for $dbkey\n" );
- $this->notFound[$dbkey] = true;
- }
- return $file;
- }
-
- /**
- * Search the cache for multiple files.
- * @param array $titles Title objects or strings to search for
- * @return array of File objects, indexed by DB key
- */
- function findFiles( $titles ) {
- $titleObjs = array();
- foreach ( $titles as $title ) {
- if ( !( $title instanceof Title ) ) {
- $title = Title::makeTitleSafe( NS_FILE, $title );
- }
- if ( $title ) {
- $titleObjs[$title->getDBkey()] = $title;
- }
- }
-
- $result = array_intersect_key( $this->cache, $titleObjs );
-
- $unsure = array_diff_key( $titleObjs, $result, $this->notFound );
- if( $unsure ) {
- wfDebug( "FileCache MISS for ".count( $unsure )." files out of ".count( $titleObjs )."...\n" );
- // XXX: We assume the array returned by findFiles() is
- // indexed by DBkey; this appears to be true, but should
- // be explicitly documented.
- $found = $this->repoGroup->findFiles( $unsure );
- $result += $found;
- $this->addFiles( $found );
- $this->markNotFound( array_keys( array_diff_key( $unsure, $found ) ) );
- }
-
- wfDebug( "FileCache found ".count( $result )." files out of ".count( $titleObjs )."\n" );
- return $result;
- }
-}
diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php
index c9d34377..f94709b3 100644
--- a/includes/filerepo/FileRepo.php
+++ b/includes/filerepo/FileRepo.php
@@ -6,16 +6,15 @@
* @ingroup FileRepo
*/
abstract class FileRepo {
+ const FILES_ONLY = 1;
const DELETE_SOURCE = 1;
- const FIND_PRIVATE = 1;
- const FIND_IGNORE_REDIRECT = 2;
const OVERWRITE = 2;
const OVERWRITE_SAME = 4;
var $thumbScriptUrl, $transformVia404;
var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
var $pathDisclosureProtection = 'paranoid';
- var $descriptionCacheExpiry, $apiThumbCacheExpiry, $hashLevels;
+ var $descriptionCacheExpiry, $hashLevels, $url, $thumbUrl;
/**
* Factory functions for creating new files
@@ -29,10 +28,10 @@ abstract class FileRepo {
$this->name = $info['name'];
// Optional settings
- $this->initialCapital = true; // by default
+ $this->initialCapital = MWNamespace::isCapitalized( NS_FILE );
foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
- 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection',
- 'descriptionCacheExpiry', 'apiThumbCacheExpiry', 'hashLevels' ) as $var )
+ 'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection',
+ 'descriptionCacheExpiry', 'hashLevels', 'url', 'thumbUrl' ) as $var )
{
if ( isset( $info[$var] ) ) {
$this->$var = $info[$var];
@@ -81,9 +80,24 @@ abstract class FileRepo {
* 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, $flags = 0 ) {
+ * @param $options Associative array of options:
+ * 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.
+ *
+ * ignoreRedirect: If true, do not follow file redirects
+ *
+ * private: If true, return restricted (deleted) files if the current
+ * user is allowed to view them. Otherwise, such files will not
+ * be found.
+ */
+ function findFile( $title, $options = array() ) {
+ if ( !is_array( $options ) ) {
+ // MW 1.15 compat
+ $time = $options;
+ } else {
+ $time = isset( $options['time'] ) ? $options['time'] : false;
+ }
if ( !($title instanceof Title) ) {
$title = Title::makeTitleSafe( NS_FILE, $title );
if ( !is_object( $title ) ) {
@@ -104,17 +118,17 @@ abstract class FileRepo {
if ( $img && $img->exists() ) {
if ( !$img->isDeleted(File::DELETED_FILE) ) {
return $img;
- } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
+ } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) {
return $img;
}
}
}
-
+
# Now try redirects
- if ( $flags & FileRepo::FIND_IGNORE_REDIRECT ) {
+ if ( !empty( $options['ignoreRedirect'] ) ) {
return false;
}
- $redir = $this->checkRedirect( $title );
+ $redir = $this->checkRedirect( $title );
if( $redir && $redir->getNamespace() == NS_FILE) {
$img = $this->newFile( $redir );
if( !$img ) {
@@ -127,22 +141,34 @@ abstract class FileRepo {
}
return false;
}
-
+
/*
- * Find many files at once.
- * @param array $titles, an array of titles
- * @todo Think of a good way to optionally pass timestamps to this function.
+ * Find many files at once.
+ * @param array $items, an array of titles, or an array of findFile() options with
+ * the "title" option giving the title. Example:
+ *
+ * $findItem = array( 'title' => $title, 'private' => true );
+ * $findBatch = array( $findItem );
+ * $repo->findFiles( $findBatch );
*/
- function findFiles( $titles ) {
+ function findFiles( $items ) {
$result = array();
- foreach ( $titles as $index => $title ) {
- $file = $this->findFile( $title );
+ foreach ( $items as $index => $item ) {
+ if ( is_array( $item ) ) {
+ $title = $item['title'];
+ $options = $item;
+ unset( $options['title'] );
+ } else {
+ $title = $item;
+ $options = array();
+ }
+ $file = $this->findFile( $title, $options );
if ( $file )
$result[$file->getTitle()->getDBkey()] = $file;
}
return $result;
}
-
+
/**
* Create a new File object from the local repository
* @param mixed $sha1 SHA-1 key
@@ -163,16 +189,23 @@ abstract class FileRepo {
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
+ * @param array $options Option array, same as findFile().
*/
- function findFileFromKey( $sha1, $time = false, $flags = 0 ) {
+ function findFileFromKey( $sha1, $options = array() ) {
+ if ( !is_array( $options ) ) {
+ # MW 1.15 compat
+ $time = $options;
+ } else {
+ $time = isset( $options['time'] ) ? $options['time'] : false;
+ }
+
# First try the current version of the file to see if it precedes the timestamp
$img = $this->newFileFromKey( $sha1 );
if ( !$img ) {
@@ -187,7 +220,7 @@ abstract class FileRepo {
if ( $img->exists() ) {
if ( !$img->isDeleted(File::DELETED_FILE) ) {
return $img;
- } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
+ } else if ( !empty( $options['private'] ) && $img->userCan(File::DELETED_FILE) ) {
return $img;
}
}
@@ -203,6 +236,15 @@ abstract class FileRepo {
}
/**
+ * Get the URL corresponding to one of the four basic zones
+ * @param String $zone One of: public, deleted, temp, thumb
+ * @return String or false
+ */
+ function getZoneUrl( $zone ) {
+ return false;
+ }
+
+ /**
* Returns true if the repository can transform files via a 404 handler
*/
function canTransformVia404() {
@@ -214,7 +256,7 @@ abstract class FileRepo {
*/
function getNameFromTitle( $title ) {
global $wgCapitalLinks;
- if ( $this->initialCapital != $wgCapitalLinks ) {
+ if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
global $wgContLang;
$name = $title->getUserCaseDBKey();
if ( $this->initialCapital ) {
@@ -238,7 +280,7 @@ abstract class FileRepo {
return $path;
}
}
-
+
/**
* Get a relative path including trailing slash, e.g. f/fa/
* If the repo is not hashed, returns an empty string
@@ -355,6 +397,17 @@ abstract class FileRepo {
*/
abstract function storeTemp( $originalName, $srcPath );
+
+ /**
+ * Append the contents of the source path to the given file.
+ * @param $srcPath string location of the source file
+ * @param $toAppendPath string path to append to.
+ * @param $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
+ * that the source file should be deleted if possible
+ * @return mixed Status or false
+ */
+ abstract function append( $srcPath, $toAppendPath, $flags = 0 );
+
/**
* Remove a temporary file or mark it for garbage collection
* @param string $virtualUrl The virtual URL returned by storeTemp
@@ -400,6 +453,21 @@ abstract class FileRepo {
*/
abstract function publishBatch( $triplets, $flags = 0 );
+ function fileExists( $file, $flags = 0 ) {
+ $result = $this->fileExistsBatch( array( $file ), $flags );
+ return $result[0];
+ }
+
+ /**
+ * Checks existence of an array of files.
+ *
+ * @param array $files URLs (or paths) of files to check
+ * @param integer $flags Bitwise combination of the following flags:
+ * self::FILES_ONLY Mark file as existing only if it is a file (not directory)
+ * @return Either array of files and existence flags, or false
+ */
+ abstract function fileExistsBatch( $files, $flags = 0 );
+
/**
* Move a group of files to the deletion archive.
*
@@ -529,21 +597,25 @@ abstract class FileRepo {
/**
* Invalidates image redirect cache related to that image
+ * Doesn't do anything for repositories that don't support image redirects.
*
+ * STUB
* @param Title $title Title of image
- */
- function invalidateImageRedirect( $title ) {
- global $wgMemc;
- $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
- $wgMemc->delete( $memcKey );
- }
-
+ */
+ function invalidateImageRedirect( $title ) {}
+
+ /**
+ * Get an array or iterator of file objects for files that have a given
+ * SHA-1 content hash.
+ *
+ * STUB
+ */
function findBySha1( $hash ) {
return array();
}
-
+
/**
- * Get the human-readable name of the repo.
+ * Get the human-readable name of the repo.
* @return string
*/
public function getDisplayName() {
@@ -551,22 +623,33 @@ abstract class FileRepo {
if ( $this->name == 'local' ) {
return null;
}
+ // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
$repoName = wfMsg( 'shared-repo-name-' . $this->name );
if ( !wfEmptyMsg( 'shared-repo-name-' . $this->name, $repoName ) ) {
return $repoName;
}
- return wfMsg( 'shared-repo' );
- }
-
- function getSlaveDB() {
- return wfGetDB( DB_SLAVE );
+ return wfMsg( 'shared-repo' );
}
- function getMasterDB() {
- return wfGetDB( DB_MASTER );
+ /**
+ * Get a key on the primary cache for this repository.
+ * Returns false if the repository's cache is not accessible at this site.
+ * The parameters are the parts of the key, as for wfMemcKey().
+ *
+ * STUB
+ */
+ function getSharedCacheKey( /*...*/ ) {
+ return false;
}
-
- function getMemcKey( $key ) {
- return wfWikiID( $this->getSlaveDB() ) . ":{$key}";
+
+ /**
+ * Get a key for this repo in the local cache domain. These cache keys are
+ * not shared with remote instances of the repo.
+ * The parameters are the parts of the key, as for wfMemcKey().
+ */
+ function getLocalCacheKey( /*...*/ ) {
+ $args = func_get_args();
+ array_unshift( $args, 'filerepo', $this->getName() );
+ return call_user_func_array( 'wfMemcKey', $args );
}
}
diff --git a/includes/filerepo/ForeignAPIFile.php b/includes/filerepo/ForeignAPIFile.php
index 03498fb1..c46b1f8f 100644
--- a/includes/filerepo/ForeignAPIFile.php
+++ b/includes/filerepo/ForeignAPIFile.php
@@ -43,10 +43,7 @@ class ForeignAPIFile extends File {
$this->getName(),
isset( $params['width'] ) ? $params['width'] : -1,
isset( $params['height'] ) ? $params['height'] : -1 );
- if( $thumbUrl ) {
- return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );;
- }
- return false;
+ return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );;
}
// Info we can get from API...
@@ -108,7 +105,7 @@ class ForeignAPIFile extends File {
return $this->mInfo['mime'];
}
- /// @fixme May guess wrong on file types that can be eg audio or video
+ /// @todo 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() );
@@ -162,13 +159,13 @@ class ForeignAPIFile extends File {
function purgeDescriptionPage() {
global $wgMemc, $wgContLang;
$url = $this->repo->getDescriptionRenderUrl( $this->getName(), $wgContLang->getCode() );
- $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($url) );
+ $key = $this->repo->getLocalCacheKey( 'RemoteFileDescription', 'url', md5($url) );
$wgMemc->delete( $key );
}
function purgeThumbnails() {
global $wgMemc;
- $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
+ $key = $this->repo->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $this->getName() );
$wgMemc->delete( $key );
$files = $this->getThumbnails();
$dir = $this->getThumbPath( $this->getName() );
diff --git a/includes/filerepo/ForeignAPIRepo.php b/includes/filerepo/ForeignAPIRepo.php
index e63e4a6b..264cb920 100644
--- a/includes/filerepo/ForeignAPIRepo.php
+++ b/includes/filerepo/ForeignAPIRepo.php
@@ -19,18 +19,30 @@
*/
class ForeignAPIRepo extends FileRepo {
var $fileFactory = array( 'ForeignAPIFile', 'newFromTitle' );
- var $apiThumbCacheExpiry = 0;
+ var $apiThumbCacheExpiry = 86400;
protected $mQueryCache = array();
-
+ protected $mFileExists = array();
+
function __construct( $info ) {
parent::__construct( $info );
$this->mApiBase = $info['apibase']; // http://commons.wikimedia.org/w/api.php
+ if( isset( $info['apiThumbCacheExpiry'] ) ) {
+ $this->apiThumbCacheExpiry = $info['apiThumbCacheExpiry'];
+ }
if( !$this->scriptDirUrl ) {
// hack for description fetches
$this->scriptDirUrl = dirname( $this->mApiBase );
}
+ // If we can cache thumbs we can guess sane defaults for these
+ if( $this->canCacheThumbs() && !$this->url ) {
+ global $wgLocalFileRepo;
+ $this->url = $wgLocalFileRepo['url'];
+ }
+ if( $this->canCacheThumbs() && !$this->thumbUrl ) {
+ $this->thumbUrl = $this->url . '/thumb';
+ }
}
-
+
/**
* Per docs in FileRepo, this needs to return false if we don't support versioned
* files. Well, we don't.
@@ -51,19 +63,49 @@ class ForeignAPIRepo extends FileRepo {
function storeTemp( $originalName, $srcPath ) {
return false;
}
+ function append( $srcPath, $toAppendPath, $flags = 0 ){
+ return false;
+ }
function publishBatch( $triplets, $flags = 0 ) {
return false;
}
function deleteBatch( $sourceDestPairs ) {
return false;
}
+
+
+ function fileExistsBatch( $files, $flags = 0 ) {
+ $results = array();
+ foreach ( $files as $k => $f ) {
+ if ( isset( $this->mFileExists[$k] ) ) {
+ $results[$k] = true;
+ unset( $files[$k] );
+ } elseif( self::isVirtualUrl( $f ) ) {
+ # TODO! FIXME! We need to be able to handle virtual
+ # URLs better, at least when we know they refer to the
+ # same repo.
+ $results[$k] = false;
+ unset( $files[$k] );
+ }
+ }
+
+ $results = $this->fetchImageQuery( array( 'titles' => implode( $files, '|' ),
+ 'prop' => 'imageinfo' ) );
+ if( isset( $data['query']['pages'] ) ) {
+ $i = 0;
+ foreach( $files as $key => $file ) {
+ $results[$key] = $this->mFileExists[$key] = !isset( $data['query']['pages'][$i]['missing'] );
+ $i++;
+ }
+ }
+ }
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] ) ) {
@@ -73,10 +115,10 @@ class ForeignAPIRepo extends FileRepo {
}
return false;
}
-
+
protected function fetchImageQuery( $query ) {
global $wgMemc;
-
+
$url = $this->mApiBase .
'?' .
wfArrayToCgi(
@@ -84,9 +126,9 @@ class ForeignAPIRepo extends FileRepo {
array(
'format' => 'json',
'action' => 'query' ) ) );
-
+
if( !isset( $this->mQueryCache[$url] ) ) {
- $key = wfMemcKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) );
+ $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'Metadata', md5( $url ) );
$data = $wgMemc->get( $key );
if( !$data ) {
$data = Http::get( $url );
@@ -102,16 +144,16 @@ class ForeignAPIRepo extends FileRepo {
}
$this->mQueryCache[$url] = $data;
}
- return json_decode( $this->mQueryCache[$url], true );
+ return FormatJson::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',
'prop' => 'imageinfo' ) );
}
-
+
function findBySha1( $hash ) {
$results = $this->fetchImageQuery( array(
'aisha1base36' => $hash,
@@ -125,7 +167,7 @@ class ForeignAPIRepo extends FileRepo {
}
return $ret;
}
-
+
function getThumbUrl( $name, $width=-1, $height=-1 ) {
$info = $this->queryImage( array(
'titles' => 'Image:' . $name,
@@ -133,49 +175,69 @@ class ForeignAPIRepo extends FileRepo {
'iiurlwidth' => $width,
'iiurlheight' => $height,
'prop' => 'imageinfo' ) );
- if( $info ) {
+ if( $info && $info['thumburl'] ) {
wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" );
return $info['thumburl'];
} else {
return false;
}
}
-
+
function getThumbUrlFromCache( $name, $width, $height ) {
global $wgMemc, $wgUploadPath, $wgServer, $wgUploadDirectory;
-
+
if ( !$this->canCacheThumbs() ) {
return $this->getThumbUrl( $name, $width, $height );
}
-
- $key = wfMemcKey( 'ForeignAPIRepo', 'ThumbUrl', $name );
+
+ $key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $name );
if ( $thumbUrl = $wgMemc->get($key) ) {
wfDebug("Got thumb from local cache. $thumbUrl \n");
return $thumbUrl;
}
else {
$foreignUrl = $this->getThumbUrl( $name, $width, $height );
-
+ if( !$foreignUrl ) {
+ wfDebug( __METHOD__ . " Could not find thumburl\n" );
+ return false;
+ }
+ $thumb = Http::get( $foreignUrl );
+ if( !$thumb ) {
+ wfDebug( __METHOD__ . " Could not download thumb\n" );
+ return false;
+ }
// We need the same filename as the remote one :)
- $fileName = ltrim( substr( $foreignUrl, strrpos( $foreignUrl, '/' ) ), '/' );
+ $fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) );
$path = 'thumb/' . $this->getHashPath( $name ) . $name . "/";
if ( !is_dir($wgUploadDirectory . '/' . $path) ) {
wfMkdirParents($wgUploadDirectory . '/' . $path);
}
- if ( !is_writable( $wgUploadDirectory . '/' . $path . $fileName ) ) {
+ $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName;
+ # FIXME: Delete old thumbs that aren't being used. Maintenance script?
+ if( !file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb ) ) {
wfDebug( __METHOD__ . " could not write to thumb path\n" );
return $foreignUrl;
}
- $localUrl = $wgServer . $wgUploadPath . '/' . $path . $fileName;
- $thumb = Http::get( $foreignUrl );
- # FIXME: Delete old thumbs that aren't being used. Maintenance script?
- file_put_contents($wgUploadDirectory . '/' . $path . $fileName, $thumb );
$wgMemc->set( $key, $localUrl, $this->apiThumbCacheExpiry );
wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" );
return $localUrl;
}
}
-
+
+ /**
+ * @see FileRepo::getZoneUrl()
+ */
+ function getZoneUrl( $zone ) {
+ switch ( $zone ) {
+ case 'public':
+ return $this->url;
+ case 'thumb':
+ return $this->thumbUrl;
+ default:
+ return parent::getZoneUrl( $zone );
+ }
+ }
+
/**
* Are we locally caching the thumbnails?
* @return bool
diff --git a/includes/filerepo/ForeignDBFile.php b/includes/filerepo/ForeignDBFile.php
index 8fe6f921..a24ff72b 100644
--- a/includes/filerepo/ForeignDBFile.php
+++ b/includes/filerepo/ForeignDBFile.php
@@ -19,16 +19,6 @@ class ForeignDBFile extends LocalFile {
return $file;
}
- function getCacheKey() {
- if ( $this->repo->hasSharedCache() ) {
- $hashedName = md5($this->name);
- return wfForeignMemcKey( $this->repo->dbName, $this->repo->tablePrefix,
- 'file', $hashedName );
- } else {
- return false;
- }
- }
-
function publish( $srcPath, $flags = 0 ) {
$this->readOnlyError();
}
diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php
index e078dd25..35c2c4bf 100644
--- a/includes/filerepo/ForeignDBRepo.php
+++ b/includes/filerepo/ForeignDBRepo.php
@@ -44,6 +44,21 @@ class ForeignDBRepo extends LocalRepo {
return $this->hasSharedCache;
}
+ /**
+ * Get a key on the primary cache for this repository.
+ * Returns false if the repository's cache is not accessible at this site.
+ * The parameters are the parts of the key, as for wfMemcKey().
+ */
+ function getSharedCacheKey( /*...*/ ) {
+ if ( $this->hasSharedCache() ) {
+ $args = func_get_args();
+ array_unshift( $args, $this->dbName, $this->tablePrefix );
+ return call_user_func_array( 'wfForeignMemcKey', $args );
+ } else {
+ return false;
+ }
+ }
+
function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
throw new MWException( get_class($this) . ': write operations are not supported' );
}
diff --git a/includes/filerepo/ForeignDBViaLBRepo.php b/includes/filerepo/ForeignDBViaLBRepo.php
index 13c9f434..80325752 100644
--- a/includes/filerepo/ForeignDBViaLBRepo.php
+++ b/includes/filerepo/ForeignDBViaLBRepo.php
@@ -27,6 +27,21 @@ class ForeignDBViaLBRepo extends LocalRepo {
return $this->hasSharedCache;
}
+ /**
+ * Get a key on the primary cache for this repository.
+ * Returns false if the repository's cache is not accessible at this site.
+ * The parameters are the parts of the key, as for wfMemcKey().
+ */
+ function getSharedCacheKey( /*...*/ ) {
+ if ( $this->hasSharedCache() ) {
+ $args = func_get_args();
+ array_unshift( $args, $this->wiki );
+ return implode( ':', $args );
+ } else {
+ return false;
+ }
+ }
+
function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
throw new MWException( get_class($this) . ': write operations are not supported' );
}
diff --git a/includes/filerepo/Image.php b/includes/filerepo/Image.php
index 5207bb4b..08ce219a 100644
--- a/includes/filerepo/Image.php
+++ b/includes/filerepo/Image.php
@@ -19,7 +19,7 @@ class Image extends LocalFile {
*/
static function newFromTitle( $title, $time = false ) {
wfDeprecated( __METHOD__ );
- $img = wfFindFile( $title, $time );
+ $img = wfFindFile( $title, array( 'time' => $time ) );
if ( !$img ) {
$img = wfLocalFile( $title );
}
@@ -44,7 +44,7 @@ class Image extends LocalFile {
}
return $img;
} else {
- return NULL;
+ return null;
}
}
diff --git a/includes/filerepo/LocalFile.php b/includes/filerepo/LocalFile.php
index b997d75f..b6b4bfed 100644
--- a/includes/filerepo/LocalFile.php
+++ b/includes/filerepo/LocalFile.php
@@ -24,8 +24,7 @@ define( 'MW_FILE_VERSION', 8 );
*
* @ingroup FileRepo
*/
-class LocalFile extends File
-{
+class LocalFile extends File {
/**#@+
* @private
*/
@@ -49,6 +48,7 @@ class LocalFile extends 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
+ $missing, # True if file is not present in file system. Not to be cached in memcached
$deleted; # Bitfield akin to rev_deleted
/**#@-*/
@@ -122,7 +122,7 @@ class LocalFile extends File
*/
function __construct( $title, $repo ) {
if( !is_object( $title ) ) {
- throw new MWException( __CLASS__.' constructor given bogus title.' );
+ throw new MWException( __CLASS__ . ' constructor given bogus title.' );
}
parent::__construct( $title, $repo );
$this->metadata = '';
@@ -132,11 +132,12 @@ class LocalFile extends File
}
/**
- * Get the memcached key
+ * Get the memcached key for the main data for this file, or false if
+ * there is no access to the shared cache.
*/
function getCacheKey() {
- $hashedName = md5($this->getName());
- return wfMemcKey( 'file', $hashedName );
+ $hashedName = md5( $this->getName() );
+ return $this->repo->getSharedCacheKey( 'file', $hashedName );
}
/**
@@ -148,12 +149,13 @@ class LocalFile extends File
$this->dataLoaded = false;
$key = $this->getCacheKey();
if ( !$key ) {
+ wfProfileOut( __METHOD__ );
return false;
}
$cachedValues = $wgMemc->get( $key );
// Check if the key existed and belongs to this version of MediaWiki
- if ( isset($cachedValues['version']) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
+ if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
wfDebug( "Pulling file metadata from cache key $key\n" );
$this->fileExists = $cachedValues['fileExists'];
if ( $this->fileExists ) {
@@ -250,7 +252,7 @@ class LocalFile extends File
$prefixLength = strlen( $prefix );
// Sanity check prefix once
if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
- throw new MWException( __METHOD__. ': incorrect $prefix parameter' );
+ throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
}
$decoded = array();
foreach ( $array as $name => $value ) {
@@ -258,19 +260,19 @@ class LocalFile extends File
}
$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
if ( empty( $decoded['major_mime'] ) ) {
- $decoded['mime'] = "unknown/unknown";
+ $decoded['mime'] = 'unknown/unknown';
} else {
- if (!$decoded['minor_mime']) {
- $decoded['minor_mime'] = "unknown";
+ if ( !$decoded['minor_mime'] ) {
+ $decoded['minor_mime'] = 'unknown';
}
- $decoded['mime'] = $decoded['major_mime'].'/'.$decoded['minor_mime'];
+ $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
}
# Trim zero padding from char/binary field
$decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
return $decoded;
}
- /*
+ /**
* Load file metadata from a DB result row
*/
function loadFromRow( $row, $prefix = 'img_' ) {
@@ -303,7 +305,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();
@@ -331,16 +333,18 @@ class LocalFile extends File
# Don't destroy file info of missing files
if ( !$this->fileExists ) {
- wfDebug( __METHOD__.": file does not exist, aborting\n" );
+ wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
+ wfProfileOut( __METHOD__ );
return;
}
$dbw = $this->repo->getMasterDB();
list( $major, $minor ) = self::splitMime( $this->mime );
if ( wfReadOnly() ) {
+ wfProfileOut( __METHOD__ );
return;
}
- wfDebug(__METHOD__.': upgrading '.$this->getName()." to the current schema\n");
+ wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
$dbw->update( 'image',
array(
@@ -391,13 +395,20 @@ class LocalFile extends File
/** getPath inherited */
/** isVisible inhereted */
+ function isMissing() {
+ if( $this->missing === null ) {
+ list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY );
+ $this->missing = !$fileExists;
+ }
+ return $this->missing;
+ }
+
/**
* Return the width of the image
*
* Returns false on error
- * @public
*/
- function getWidth( $page = 1 ) {
+ public function getWidth( $page = 1 ) {
$this->load();
if ( $this->isMultipage() ) {
$dim = $this->getHandler()->getPageDimensions( $this, $page );
@@ -415,9 +426,8 @@ class LocalFile extends File
* Return the height of the image
*
* Returns false on error
- * @public
*/
- function getHeight( $page = 1 ) {
+ public function getHeight( $page = 1 ) {
$this->load();
if ( $this->isMultipage() ) {
$dim = $this->getHandler()->getPageDimensions( $this, $page );
@@ -436,7 +446,7 @@ class LocalFile extends File
*
* @param $type string 'text' or 'id'
*/
- function getUser($type='text') {
+ function getUser( $type = 'text' ) {
$this->load();
if( $type == 'text' ) {
return $this->user_text;
@@ -460,9 +470,8 @@ class LocalFile extends File
/**
* Return the size of the image file, in bytes
- * @public
*/
- function getSize() {
+ public function getSize() {
$this->load();
return $this->size;
}
@@ -493,9 +502,8 @@ class LocalFile extends File
/**
* Returns true if the file file exists on disk.
* @return boolean Whether file file exist on disk.
- * @public
*/
- function exists() {
+ public function exists() {
$this->load();
return $this->fileExists;
}
@@ -518,7 +526,7 @@ class LocalFile extends File
// This happened occasionally due to broken migration code in 1.5
// Rename to broken-*
for ( $i = 0; $i < 100 ; $i++ ) {
- $broken = $this->repo->getZonePath('public') . "/broken-$i-$thumbName";
+ $broken = $this->repo->getZonePath( 'public' ) . "/broken-$i-$thumbName";
if ( !file_exists( $broken ) ) {
rename( $thumbPath, $broken );
break;
@@ -551,7 +559,7 @@ class LocalFile extends File
$handle = opendir( $dir );
if ( $handle ) {
- while ( false !== ( $file = readdir($handle) ) ) {
+ while ( false !== ( $file = readdir( $handle ) ) ) {
if ( $file{0} != '.' ) {
$files[] = $file;
}
@@ -577,9 +585,11 @@ class LocalFile extends File
*/
function purgeHistory() {
global $wgMemc;
- $hashedName = md5($this->getName());
- $oldKey = wfMemcKey( 'oldfile', $hashedName );
- $wgMemc->delete( $oldKey );
+ $hashedName = md5( $this->getName() );
+ $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
+ if ( $oldKey ) {
+ $wgMemc->delete( $oldKey );
+ }
}
/**
@@ -624,13 +634,13 @@ class LocalFile extends File
/** purgeDescription inherited */
/** purgeEverything inherited */
- function getHistory($limit = null, $start = null, $end = null, $inc = true) {
+ function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
$dbr = $this->repo->getSlaveDB();
- $tables = array('oldimage');
+ $tables = array( 'oldimage' );
$fields = OldLocalFile::selectFields();
$conds = $opts = $join_conds = array();
- $eq = $inc ? "=" : "";
- $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBKey() );
+ $eq = $inc ? '=' : '';
+ $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
if( $start ) {
$conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
}
@@ -641,19 +651,19 @@ class LocalFile extends File
$opts['LIMIT'] = $limit;
}
// Search backwards for time > x queries
- $order = (!$start && $end !== null) ? "ASC" : "DESC";
+ $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
$opts['ORDER BY'] = "oi_timestamp $order";
- $opts['USE INDEX'] = array('oldimage' => 'oi_name_timestamp');
-
+ $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
+
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);
+ while( $row = $dbr->fetchObject( $res ) ) {
+ $r[] = OldLocalFile::newFromRow( $row, $this->repo );
}
- if( $order == "ASC" ) {
+ if( $order == 'ASC' ) {
$r = array_reverse( $r ); // make sure it ends up descending
}
return $r;
@@ -666,10 +676,8 @@ class LocalFile extends File
* 0 return line for current version
* 1 query for old versions, return first one
* 2, ... return next old version from above query
- *
- * @public
*/
- function nextHistoryLine() {
+ public function nextHistoryLine() {
# Polymorphic function name to distinguish foreign and local fetches
$fname = get_class( $this ) . '::' . __FUNCTION__;
@@ -687,12 +695,12 @@ class LocalFile extends File
$fname
);
if ( 0 == $dbr->numRows( $this->historyRes ) ) {
- $dbr->freeResult($this->historyRes);
+ $dbr->freeResult( $this->historyRes );
$this->historyRes = null;
- return FALSE;
+ return false;
}
- } else if ( $this->historyLine == 1 ) {
- $dbr->freeResult($this->historyRes);
+ } elseif ( $this->historyLine == 1 ) {
+ $dbr->freeResult( $this->historyRes );
$this->historyRes = $dbr->select( 'oldimage', '*',
array( 'oi_name' => $this->title->getDBkey() ),
$fname,
@@ -706,12 +714,11 @@ class LocalFile extends File
/**
* Reset the history pointer to the first element of the history
- * @public
*/
- function resetHistory() {
+ public function resetHistory() {
$this->historyLine = 0;
- if (!is_null($this->historyRes)) {
- $this->repo->getSlaveDB()->freeResult($this->historyRes);
+ if ( !is_null( $this->historyRes ) ) {
+ $this->repo->getSlaveDB()->freeResult( $this->historyRes );
$this->historyRes = null;
}
}
@@ -763,7 +770,7 @@ class LocalFile extends File
function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
$watch = false, $timestamp = false )
{
- $pageText = UploadForm::getInitialPageText( $desc, $license, $copyStatus, $source );
+ $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
return false;
}
@@ -804,7 +811,7 @@ class LocalFile extends File
// Fail now if the file isn't there
if ( !$this->fileExists ) {
- wfDebug( __METHOD__.": File ".$this->getPath()." went missing!\n" );
+ wfDebug( __METHOD__ . ": File " . $this->getPath() . " went missing!\n" );
return false;
}
@@ -905,7 +912,7 @@ class LocalFile extends File
$log->getRcComment(), false );
$nullRevision->insertOn( $dbw );
- wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $user) );
+ wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) );
$article->updateRevisionOn( $dbw, $nullRevision );
# Invalidate the cache for the description page
@@ -922,7 +929,7 @@ class LocalFile extends File
# Commit the transaction now, in case something goes wrong later
# The most important thing is that files don't get lost, especially archives
- $dbw->immediateCommit();
+ $dbw->commit();
# Invalidate cache for all pages using this file
$update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
@@ -1059,7 +1066,7 @@ class LocalFile extends File
*
* @param $reason
* @param $suppress
- * @throws MWException or FSException on database or filestore failure
+ * @throws MWException or FSException on database or file store failure
* @return FileRepoStatus object.
*/
function deleteOld( $archiveName, $reason, $suppress=false ) {
@@ -1386,7 +1393,7 @@ class LocalFileDeleteBatch {
* Run the transaction
*/
function execute() {
- global $wgUser, $wgUseSquid;
+ global $wgUseSquid;
wfProfileIn( __METHOD__ );
$this->file->lock();
@@ -1399,7 +1406,7 @@ class LocalFileDeleteBatch {
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 ),
+ $dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ),
__METHOD__ );
while( $row = $dbw->fetchObject( $res ) ) {
$privateFiles[$row->oi_archive_name] = 1;
@@ -1413,7 +1420,7 @@ class LocalFileDeleteBatch {
foreach ( $this->srcRels as $name => $srcRel ) {
// Skip files that have no hash (missing source).
// Keep private files where they are.
- if ( isset($hashes[$name]) && !array_key_exists($name,$privateFiles) ) {
+ if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
$hash = $hashes[$name];
$key = $hash . $dotExt;
$dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
@@ -1429,6 +1436,9 @@ class LocalFileDeleteBatch {
// them in a separate transaction, then run the file ops, then update the fa_name fields.
$this->doDBInserts();
+ // Removes non-existent file from the batch, so we don't get errors.
+ $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
+
// Execute the file deletion batch
$status = $this->file->repo->deleteBatch( $this->deletionBatch );
if ( !$status->isGood() ) {
@@ -1440,6 +1450,7 @@ class LocalFileDeleteBatch {
// Roll back inserts, release lock and abort
// TODO: delete the defunct filearchive rows if we are using a non-transactional DB
$this->file->unlockAndRollback();
+ wfProfileOut( __METHOD__ );
return $this->status;
}
@@ -1461,6 +1472,22 @@ class LocalFileDeleteBatch {
wfProfileOut( __METHOD__ );
return $this->status;
}
+
+ /**
+ * Removes non-existent files from a deletion batch.
+ */
+ function removeNonexistentFiles( $batch ) {
+ $files = $newBatch = array();
+ foreach( $batch as $batchItem ) {
+ list( $src, $dest ) = $batchItem;
+ $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
+ }
+ $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+ foreach( $batch as $batchItem )
+ if( $result[$batchItem[0]] )
+ $newBatch[] = $batchItem;
+ return $newBatch;
+ }
}
#------------------------------------------------------------------------------
@@ -1508,7 +1535,7 @@ class LocalFileRestoreBatch {
* So we save the batch and let the caller call cleanup()
*/
function execute() {
- global $wgUser, $wgLang;
+ global $wgLang;
if ( !$this->all && !$this->ids ) {
// Do nothing
return $this->file->repo->newGood();
@@ -1653,6 +1680,9 @@ class LocalFileRestoreBatch {
$status->error( 'undelete-missing-filearchive', $id );
}
+ // Remove missing files from batch, so we don't get errors when undeleting them
+ $storeBatch = $this->removeNonexistentFiles( $storeBatch );
+
// Run the store batch
// Use the OVERWRITE_SAME flag to smooth over a common error
$storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
@@ -1683,9 +1713,10 @@ class LocalFileRestoreBatch {
__METHOD__ );
}
- if( $status->successCount > 0 ) {
+ // If store batch is empty (all files are missing), deletion is to be considered successful
+ if( $status->successCount > 0 || !$storeBatch ) {
if( !$exists ) {
- wfDebug( __METHOD__." restored {$status->successCount} items, creating a new current\n" );
+ wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
// Update site_stats
$site_stats = $dbw->tableName( 'site_stats' );
@@ -1693,7 +1724,7 @@ class LocalFileRestoreBatch {
$this->file->purgeEverything();
} else {
- wfDebug( __METHOD__." restored {$status->successCount} as archived versions\n" );
+ wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
$this->file->purgeDescription();
$this->file->purgeHistory();
}
@@ -1703,6 +1734,38 @@ class LocalFileRestoreBatch {
}
/**
+ * Removes non-existent files from a store batch.
+ */
+ function removeNonexistentFiles( $triplets ) {
+ $files = $filteredTriplets = array();
+ foreach( $triplets as $file )
+ $files[$file[0]] = $file[0];
+ $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+ foreach( $triplets as $file )
+ if( $result[$file[0]] )
+ $filteredTriplets[] = $file;
+ return $filteredTriplets;
+ }
+
+ /**
+ * Removes non-existent files from a cleanup batch.
+ */
+ function removeNonexistentFromCleanup( $batch ) {
+ $files = $newBatch = array();
+ $repo = $this->file->repo;
+ foreach( $batch as $file ) {
+ $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
+ rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
+ }
+
+ $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+ foreach( $batch as $file )
+ if( $result[$file] )
+ $newBatch[] = $file;
+ return $newBatch;
+ }
+
+ /**
* Delete unused files in the deleted zone.
* This should be called from outside the transaction in which execute() was called.
*/
@@ -1710,6 +1773,7 @@ class LocalFileRestoreBatch {
if ( !$this->cleanupBatch ) {
return $this->file->repo->newGood();
}
+ $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
$status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
return $status;
}
@@ -1728,7 +1792,7 @@ class LocalFileMoveBatch {
$this->file = $file;
$this->target = $target;
$this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
- $this->newHash = $this->file->repo->getHashPath( $this->target->getDBKey() );
+ $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
$this->oldName = $this->file->getName();
$this->newName = $this->file->repo->getNameFromTitle( $this->target );
$this->oldRel = $this->oldHash . $this->oldName;
@@ -1736,14 +1800,14 @@ class LocalFileMoveBatch {
$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() {
@@ -1781,7 +1845,7 @@ class LocalFileMoveBatch {
$this->db->freeResult( $result );
}
- /*
+ /**
* Perform the move.
*/
function execute() {
@@ -1789,6 +1853,7 @@ class LocalFileMoveBatch {
$status = $repo->newGood();
$triplets = $this->getMoveTriplets();
+ $triplets = $this->removeNonexistentFiles( $triplets );
$statusDb = $this->doDBUpdates();
wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
$statusMove = $repo->storeBatch( $triplets, FSRepo::DELETE_SOURCE );
@@ -1797,12 +1862,13 @@ class LocalFileMoveBatch {
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.
*/
@@ -1842,7 +1908,7 @@ class LocalFileMoveBatch {
return $status;
}
- /*
+ /**
* Generate triplets for FSRepo::storeBatch().
*/
function getMoveTriplets() {
@@ -1856,4 +1922,22 @@ class LocalFileMoveBatch {
}
return $triplets;
}
+
+ /**
+ * Removes non-existent files from move batch.
+ */
+ function removeNonexistentFiles( $triplets ) {
+ $files = array();
+ foreach( $triplets as $file )
+ $files[$file[0]] = $file[0];
+ $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
+ $filteredTriplets = array();
+ foreach( $triplets as $file )
+ if( $result[$file[0]] ) {
+ $filteredTriplets[] = $file;
+ } else {
+ wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
+ }
+ return $filteredTriplets;
+ }
}
diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php
index c679dd98..6c4d21a2 100644
--- a/includes/filerepo/LocalRepo.php
+++ b/includes/filerepo/LocalRepo.php
@@ -49,8 +49,8 @@ class LocalRepo extends FSRepo {
$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 ),
+ 'oi_archive_name ' . $dbw->buildLike( $dbw->anyString(), ".$ext" ),
+ $dbw->bitAnd('oi_deleted', File::DELETED_FILE) => File::DELETED_FILE ),
__METHOD__, array( 'FOR UPDATE' ) );
}
if ( !$inuse ) {
@@ -83,17 +83,24 @@ class LocalRepo extends FSRepo {
$title = Title::makeTitle( NS_FILE, $title->getText() );
}
- $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
+ $memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) );
+ if ( $memcKey === false ) {
+ $memcKey = $this->getLocalCacheKey( 'image_redirect', md5( $title->getDBkey() ) );
+ $expiry = 300; // no invalidation, 5 minutes
+ } else {
+ $expiry = 86400; // has invalidation, 1 day
+ }
$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
+ if ( $cachedValue === ' ' || $cachedValue === '' ) {
+ // Does not exist
return false;
- }
+ } elseif ( strval( $cachedValue ) !== '' ) {
+ return Title::newFromText( $cachedValue, NS_FILE );
+ } // else $cachedValue is false or null: cache miss
$id = $this->getArticleID( $title );
if( !$id ) {
- $wgMemc->set( $memcKey, " ", 9000 );
+ $wgMemc->set( $memcKey, " ", $expiry );
return false;
}
$dbr = $this->getSlaveDB();
@@ -104,12 +111,14 @@ class LocalRepo extends FSRepo {
__METHOD__
);
- if( $row ) $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title );
- $wgMemc->set( $memcKey, ($row ? $targetTitle->getPrefixedDBkey() : " "), 9000 );
- if( !$row ) {
+ if( $row && $row->rd_namespace == NS_FILE ) {
+ $targetTitle = Title::makeTitle( $row->rd_namespace, $row->rd_title );
+ $wgMemc->set( $memcKey, $targetTitle->getDBkey(), $expiry );
+ return $targetTitle;
+ } else {
+ $wgMemc->set( $memcKey, '', $expiry );
return false;
}
- return $targetTitle;
}
@@ -127,15 +136,17 @@ class LocalRepo extends FSRepo {
'page_id', //Field
array( //Conditions
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBKey(),
+ 'page_title' => $title->getDBkey(),
),
__METHOD__ //Function name
);
return $id;
}
-
-
+ /**
+ * Get an array or iterator of file objects for files that have a given
+ * SHA-1 content hash.
+ */
function findBySha1( $hash ) {
$dbr = $this->getSlaveDB();
$res = $dbr->select(
@@ -150,28 +161,42 @@ class LocalRepo extends FSRepo {
$res->free();
return $result;
}
-
- /*
- * Find many files using one query
+
+ /**
+ * Get a connection to the slave DB
*/
- function findFiles( $titles ) {
- // 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 );
+ function getSlaveDB() {
+ return wfGetDB( DB_SLAVE );
+ }
+
+ /**
+ * Get a connection to the master DB
+ */
+ function getMasterDB() {
+ return wfGetDB( DB_MASTER );
+ }
+
+ /**
+ * Get a key on the primary cache for this repository.
+ * Returns false if the repository's cache is not accessible at this site.
+ * The parameters are the parts of the key, as for wfMemcKey().
+ */
+ function getSharedCacheKey( /*...*/ ) {
+ $args = func_get_args();
+ return call_user_func_array( 'wfMemcKey', $args );
+ }
+
+ /**
+ * Invalidates image redirect cache related to that image
+ *
+ * @param Title $title Title of image
+ */
+ function invalidateImageRedirect( $title ) {
+ global $wgMemc;
+ $memcKey = $this->getSharedCacheKey( 'image_redirect', md5( $title->getDBkey() ) );
+ if ( $memcKey ) {
+ $wgMemc->delete( $memcKey );
}
- $res->free();
- return $result;
}
}
+
diff --git a/includes/filerepo/NullRepo.php b/includes/filerepo/NullRepo.php
index fb89cebb..2bc61bde 100644
--- a/includes/filerepo/NullRepo.php
+++ b/includes/filerepo/NullRepo.php
@@ -14,19 +14,25 @@ class NullRepo extends FileRepo {
function storeTemp( $originalName, $srcPath ) {
return false;
}
+ function append( $srcPath, $toAppendPath, $flags = 0 ){
+ return false;
+ }
function publishBatch( $triplets, $flags = 0 ) {
return false;
}
function deleteBatch( $sourceDestPairs ) {
return false;
}
+ function fileExistsBatch( $files, $flags = 0 ) {
+ return false;
+ }
function getFileProps( $virtualUrl ) {
return false;
}
function newFile( $title, $time = false ) {
return false;
}
- function findFile( $title, $time = false ) {
+ function findFile( $title, $options = array() ) {
return false;
}
}
diff --git a/includes/filerepo/OldLocalFile.php b/includes/filerepo/OldLocalFile.php
index 46c35bd9..35f3f9f2 100644
--- a/includes/filerepo/OldLocalFile.php
+++ b/includes/filerepo/OldLocalFile.php
@@ -177,25 +177,27 @@ class OldLocalFile extends LocalFile {
* @return bool
*/
function isDeleted( $field ) {
+ $this->load();
return ($this->deleted & $field) == $field;
}
/**
+ * Returns bitfield value
+ * @return int
+ */
+ function getVisibility() {
+ $this->load();
+ return (int)$this->deleted;
+ }
+
+ /**
* Determine if the current user is allowed to view a particular
- * field of this FileStore image file, if it's marked as deleted.
+ * field of this 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;
- }
+ $this->load();
+ return Revision::userCanBitfield( $this->deleted, $field );
}
}
diff --git a/includes/filerepo/RepoGroup.php b/includes/filerepo/RepoGroup.php
index 2303f581..1465400c 100644
--- a/includes/filerepo/RepoGroup.php
+++ b/includes/filerepo/RepoGroup.php
@@ -13,8 +13,10 @@
class RepoGroup {
var $localRepo, $foreignRepos, $reposInitialised = false;
var $localInfo, $foreignInfo;
+ var $cache;
protected static $instance;
+ const MAX_CACHE_SIZE = 1000;
/**
* Get a RepoGroup instance. At present only one instance of RepoGroup is
@@ -54,56 +56,116 @@ class RepoGroup {
function __construct( $localInfo, $foreignInfo ) {
$this->localInfo = $localInfo;
$this->foreignInfo = $foreignInfo;
+ $this->cache = array();
}
/**
* Search repositories for an image.
- * You can also use wfGetFile() to do this.
+ * You can also use wfFindFile() to do this.
* @param mixed $title Title object or string
- * @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
+ * @param $options Associative array of options:
+ * 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.
+ *
+ * ignoreRedirect: If true, do not follow file redirects
+ *
+ * private: If true, return restricted (deleted) files if the current
+ * user is allowed to view them. Otherwise, such files will not
+ * be found.
+ *
+ * bypassCache: If true, do not use the process-local cache of File objects
* @return File object or false if it is not found
*/
- function findFile( $title, $time = false, $flags = 0 ) {
+ function findFile( $title, $options = array() ) {
+ if ( !is_array( $options ) ) {
+ // MW 1.15 compat
+ $options = array( 'time' => $options );
+ }
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
+ if ( !($title instanceof Title) ) {
+ $title = Title::makeTitleSafe( NS_FILE, $title );
+ if ( !is_object( $title ) ) {
+ return false;
+ }
+ }
- $image = $this->localRepo->findFile( $title, $time, $flags );
+ # Check the cache
+ if ( empty( $options['ignoreRedirect'] )
+ && empty( $options['private'] )
+ && empty( $options['bypassCache'] ) )
+ {
+ $useCache = true;
+ $time = isset( $options['time'] ) ? $options['time'] : '';
+ $dbkey = $title->getDBkey();
+ if ( isset( $this->cache[$dbkey][$time] ) ) {
+ wfDebug( __METHOD__.": got File:$dbkey from process cache\n" );
+ # Move it to the end of the list so that we can delete the LRU entry later
+ $tmp = $this->cache[$dbkey];
+ unset( $this->cache[$dbkey] );
+ $this->cache[$dbkey] = $tmp;
+ # Return the entry
+ return $this->cache[$dbkey][$time];
+ } else {
+ # Add a negative cache entry, may be overridden
+ $this->trimCache();
+ $this->cache[$dbkey][$time] = false;
+ $cacheEntry =& $this->cache[$dbkey][$time];
+ }
+ } else {
+ $useCache = false;
+ }
+
+ # Check the local repo
+ $image = $this->localRepo->findFile( $title, $options );
if ( $image ) {
+ if ( $useCache ) {
+ $cacheEntry = $image;
+ }
return $image;
}
+
+ # Check the foreign repos
foreach ( $this->foreignRepos as $repo ) {
- $image = $repo->findFile( $title, $time, $flags );
+ $image = $repo->findFile( $title, $options );
if ( $image ) {
+ if ( $useCache ) {
+ $cacheEntry = $image;
+ }
return $image;
}
}
+ # Not found, do not override negative cache
return false;
}
- function findFiles( $titles ) {
+
+ function findFiles( $inputItems ) {
if ( !$this->reposInitialised ) {
$this->initialiseRepos();
}
- $titleObjs = array();
- foreach ( $titles as $title ) {
- if ( !( $title instanceof Title ) )
- $title = Title::makeTitleSafe( NS_FILE, $title );
- if ( $title )
- $titleObjs[$title->getDBkey()] = $title;
+ $items = array();
+ foreach ( $inputItems as $item ) {
+ if ( !is_array( $item ) ) {
+ $item = array( 'title' => $item );
+ }
+ if ( !( $item['title'] instanceof Title ) )
+ $item['title'] = Title::makeTitleSafe( NS_FILE, $item['title'] );
+ if ( $item['title'] )
+ $items[$item['title']->getDBkey()] = $item;
}
- $images = $this->localRepo->findFiles( $titleObjs );
+ $images = $this->localRepo->findFiles( $items );
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 ) );
+ // Remove found files from $items
+ foreach ( $images as $name => $image ) {
+ unset( $items[$name] );
+ }
+
+ $images = array_merge( $images, $repo->findFiles( $items ) );
}
return $images;
}
@@ -128,16 +190,16 @@ 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;
+ return $result;
}
/**
@@ -178,7 +240,7 @@ class RepoGroup {
}
/**
- * Call a function for each foreign repo, with the repo object as the
+ * Call a function for each foreign repo, with the repo object as the
* first parameter.
*
* @param $callback callback The function to call
@@ -254,4 +316,16 @@ class RepoGroup {
return File::getPropsFromPath( $fileName );
}
}
+
+ /**
+ * Limit cache memory
+ */
+ function trimCache() {
+ while ( count( $this->cache ) >= self::MAX_CACHE_SIZE ) {
+ reset( $this->cache );
+ $key = key( $this->cache );
+ wfDebug( __METHOD__.": evicting $key\n" );
+ unset( $this->cache[$key] );
+ }
+ }
}
diff --git a/includes/json/FormatJson.php b/includes/json/FormatJson.php
new file mode 100644
index 00000000..6db4a23f
--- /dev/null
+++ b/includes/json/FormatJson.php
@@ -0,0 +1,32 @@
+<?php
+/*
+ * simple wrapper for json_econde and json_decode that falls back on Services_JSON class
+ */
+if( !(defined( 'MEDIAWIKI' ) ) ) {
+ die( 1 );
+}
+
+class FormatJson{
+ public static function encode($value, $isHtml=false){
+ // Some versions of PHP have a broken json_encode, see PHP bug
+ // 46944. Test encoding an affected character (U+20000) to
+ // avoid this.
+ if (!function_exists('json_encode') || $isHtml || strtolower(json_encode("\xf0\xa0\x80\x80")) != '\ud840\udc00') {
+ $json = new Services_JSON();
+ return $json->encode($value, $isHtml) ;
+ } else {
+ return json_encode($value);
+ }
+ }
+ public static function decode( $value, $assoc=false ){
+ if (!function_exists('json_decode') ) {
+ $json = new Services_JSON();
+ $jsonDec = $json->decode( $value );
+ if( $assoc )
+ $jsonDec = wfObjectToArray( $jsonDec );
+ return $jsonDec;
+ } else {
+ return json_decode( $value, $assoc );
+ }
+ }
+}
diff --git a/includes/api/ApiFormatJson_json.php b/includes/json/Services_JSON.php
index 8cb3606d..94233520 100644
--- a/includes/api/ApiFormatJson_json.php
+++ b/includes/json/Services_JSON.php
@@ -50,7 +50,7 @@
* @author Matt Knapp <mdknapp[at]gmail[dot]com>
* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com>
* @copyright 2005 Michal Migurski
-* @version CVS: $Id: ApiFormatJson_json.php 45765 2009-01-15 10:18:44Z catrope $
+* @version CVS: $Id: Services_JSON.php 65683 2010-04-30 05:56:15Z tstarling $
* @license http://www.opensource.org/licenses/bsd-license.php
* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198
*/
@@ -135,6 +135,19 @@ class Services_JSON
{
$this->use = $use;
}
+
+ private static $mHavePear = null;
+ /**
+ * Returns cached result of class_exists('pear'), to avoid calling AutoLoader numerous times
+ * in cases when PEAR is not present.
+ * @return boolean
+ */
+ private static function pearInstalled() {
+ if ( self::$mHavePear === null ) {
+ self::$mHavePear = class_exists( 'pear' );
+ }
+ return self::$mHavePear;
+ }
/**
* convert a string from one UTF-16 char to one UTF-8 char
@@ -815,8 +828,9 @@ class Services_JSON
*/
function isError($data, $code = null)
{
- if (class_exists('pear')) {
- return PEAR::isError($data, $code);
+ if ( self::pearInstalled() ) {
+ //avoid some strict warnings on PEAR isError check (looks like http://pear.php.net/bugs/bug.php?id=9950 has been around for some time)
+ return @PEAR::isError($data, $code);
} elseif (is_object($data) && (get_class($data) == 'services_json_error' ||
is_subclass_of($data, 'services_json_error'))) {
return true;
diff --git a/includes/media/Bitmap.php b/includes/media/Bitmap.php
index c2f2458e..870e2126 100644
--- a/includes/media/Bitmap.php
+++ b/includes/media/Bitmap.php
@@ -22,7 +22,7 @@ class BitmapHandler extends ImageHandler {
# JPEG has the handy property of allowing thumbnailing without full decompression, so we make
# an exception for it.
if ( $mimeType !== 'image/jpeg' &&
- $srcWidth * $srcHeight > $wgMaxImageArea )
+ $this->getImageArea( $image, $srcWidth, $srcHeight ) > $wgMaxImageArea )
{
return false;
}
@@ -39,6 +39,13 @@ class BitmapHandler extends ImageHandler {
return true;
}
+
+
+ // Function that returns the number of pixels to be thumbnailed.
+ // Intended for animated GIFs to multiply by the number of frames.
+ function getImageArea( $image, $width, $height ) {
+ return $width * $height;
+ }
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
global $wgUseImageMagick, $wgImageMagickConvertCommand, $wgImageMagickTempDir;
@@ -53,6 +60,7 @@ class BitmapHandler extends ImageHandler {
$physicalHeight = $params['physicalHeight'];
$clientWidth = $params['width'];
$clientHeight = $params['height'];
+ $comment = isset( $params['descriptionUrl'] ) ? "File source: ". $params['descriptionUrl'] : '';
$srcWidth = $image->getWidth();
$srcHeight = $image->getHeight();
$mimeType = $image->getMimeType();
@@ -103,7 +111,7 @@ class BitmapHandler extends ImageHandler {
$quality = '';
$sharpen = '';
- $frame = '';
+ $scene = false;
$animation = '';
if ( $mimeType == 'image/jpeg' ) {
$quality = "-quality 80"; // 80%
@@ -117,7 +125,7 @@ class BitmapHandler extends ImageHandler {
if( $srcWidth * $srcHeight > $wgMaxAnimatedGifArea ) {
// Extract initial frame only; we're so big it'll
// be a total drag. :P
- $frame = '[0]';
+ $scene = 0;
} else {
// Coalesce is needed to scale animated GIFs properly (bug 1017).
$animation = ' -coalesce ';
@@ -139,17 +147,19 @@ class BitmapHandler extends ImageHandler {
$cmd =
$tempEnv .
- wfEscapeShellArg($wgImageMagickConvertCommand) .
+ wfEscapeShellArg( $wgImageMagickConvertCommand ) .
" {$quality} -background white -size {$physicalWidth} ".
- wfEscapeShellArg($srcPath . $frame) .
+ wfEscapeShellArg( $this->escapeMagickInput( $srcPath, $scene ) ) .
$animation .
// For the -resize option a "!" is needed to force exact size,
// or ImageMagick may decide your ratio is wrong and slice off
// a pixel.
" -thumbnail " . wfEscapeShellArg( "{$physicalWidth}x{$physicalHeight}!" ) .
+ // Add the source url as a comment to the thumb.
+ " -set comment " . wfEscapeShellArg( $this->escapeMagickProperty( $comment ) ) .
" -depth 8 $sharpen " .
- wfEscapeShellArg($dstPath) . " 2>&1";
- wfDebug( __METHOD__.": running ImageMagick: $cmd\n");
+ wfEscapeShellArg( $this->escapeMagickOutput( $dstPath ) ) . " 2>&1";
+ wfDebug( __METHOD__.": running ImageMagick: $cmd\n" );
wfProfileIn( 'convert' );
$err = wfShellExec( $cmd, $retval );
wfProfileOut( 'convert' );
@@ -181,14 +191,23 @@ class BitmapHandler extends ImageHandler {
if( !isset( $typemap[$mimeType] ) ) {
$err = 'Image type not supported';
wfDebug( "$err\n" );
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
+ $errMsg = wfMsg ( 'thumbnail_image-type' );
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
}
list( $loader, $colorStyle, $saveType ) = $typemap[$mimeType];
if( !function_exists( $loader ) ) {
$err = "Incomplete GD library configuration: missing function $loader";
wfDebug( "$err\n" );
- return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $err );
+ $errMsg = wfMsg ( 'thumbnail_gd-library', $loader );
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
+ }
+
+ if ( !file_exists( $srcPath ) ) {
+ $err = "File seems to be missing: $srcPath";
+ wfDebug( "$err\n" );
+ $errMsg = wfMsg ( 'thumbnail_image-missing', $srcPath );
+ return new MediaTransformError( 'thumbnail_error', $clientWidth, $clientHeight, $errMsg );
}
$src_image = call_user_func( $loader, $srcPath );
@@ -231,6 +250,90 @@ class BitmapHandler extends ImageHandler {
}
}
+ /**
+ * Escape a string for ImageMagick's property input (e.g. -set -comment)
+ * See InterpretImageProperties() in magick/property.c
+ */
+ function escapeMagickProperty( $s ) {
+ // Double the backslashes
+ $s = str_replace( '\\', '\\\\', $s );
+ // Double the percents
+ $s = str_replace( '%', '%%', $s );
+ // Escape initial - or @
+ if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
+ $s = '\\' . $s;
+ }
+ return $s;
+ }
+
+ /**
+ * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
+ * and GetPathComponent() in magick/utility.c.
+ *
+ * This won't work with an initial ~ or @, so input files should be prefixed
+ * with the directory name.
+ *
+ * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
+ * it's broken in a way that doesn't involve trying to convert every file
+ * in a directory, so we're better off escaping and waiting for the bugfix
+ * to filter down to users.
+ *
+ * @param $path string The file path
+ * @param $scene string The scene specification, or false if there is none
+ */
+ function escapeMagickInput( $path, $scene = false ) {
+ # Die on initial metacharacters (caller should prepend path)
+ $firstChar = substr( $path, 0, 1 );
+ if ( $firstChar === '~' || $firstChar === '@' ) {
+ throw new MWException( __METHOD__.': cannot escape this path name' );
+ }
+
+ # Escape glob chars
+ $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
+
+ return $this->escapeMagickPath( $path, $scene );
+ }
+
+ /**
+ * Escape a string for ImageMagick's output filename. See
+ * InterpretImageFilename() in magick/image.c.
+ */
+ function escapeMagickOutput( $path, $scene = false ) {
+ $path = str_replace( '%', '%%', $path );
+ return $this->escapeMagickPath( $path, $scene );
+ }
+
+ /**
+ * Armour a string against ImageMagick's GetPathComponent(). This is a
+ * helper function for escapeMagickInput() and escapeMagickOutput().
+ *
+ * @param $path string The file path
+ * @param $scene string The scene specification, or false if there is none
+ */
+ protected function escapeMagickPath( $path, $scene = false ) {
+ # Die on format specifiers (other than drive letters). The regex is
+ # meant to match all the formats you get from "convert -list format"
+ if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
+ if ( wfIsWindows() && is_dir( $m[0] ) ) {
+ // OK, it's a drive letter
+ // ImageMagick has a similar exception, see IsMagickConflict()
+ } else {
+ throw new MWException( __METHOD__.': unexpected colon character in path name' );
+ }
+ }
+
+ # If there are square brackets, add a do-nothing scene specification
+ # to force a literal interpretation
+ if ( $scene === false ) {
+ if ( strpos( $path, '[' ) !== false ) {
+ $path .= '[0--1]';
+ }
+ } else {
+ $path .= "[$scene]";
+ }
+ return $path;
+ }
+
static function imageJpegWrapper( $dst_image, $thumbPath ) {
imageinterlace( $dst_image );
imagejpeg( $dst_image, $thumbPath, 95 );
diff --git a/includes/media/DjVu.php b/includes/media/DjVu.php
index f0bbcc51..38c16c21 100644
--- a/includes/media/DjVu.php
+++ b/includes/media/DjVu.php
@@ -18,8 +18,8 @@ class DjVuHandler extends ImageHandler {
}
}
- function mustRender() { return true; }
- function isMultiPage() { return true; }
+ function mustRender( $file ) { return true; }
+ function isMultiPage( $file ) { return true; }
function getParamMap() {
return array(
@@ -135,7 +135,7 @@ class DjVuHandler extends ImageHandler {
/**
* Cache a document tree for the DjVu XML metadata
*/
- function getMetaTree( $image ) {
+ function getMetaTree( $image , $gettext = false ) {
if ( isset( $image->dejaMetaTree ) ) {
return $image->dejaMetaTree;
}
@@ -149,15 +149,32 @@ class DjVuHandler extends ImageHandler {
wfSuppressWarnings();
try {
- $image->dejaMetaTree = new SimpleXMLElement( $metadata );
- } catch( Exception $e ) {
- wfDebug( "Bogus multipage XML metadata on '$image->name'\n" );
// Set to false rather than null to avoid further attempts
$image->dejaMetaTree = false;
+ $image->djvuTextTree = false;
+ $tree = new SimpleXMLElement( $metadata );
+ if( $tree->getName() == 'mw-djvu' ) {
+ foreach($tree->children() as $b){
+ if( $b->getName() == 'DjVuTxt' ) {
+ $image->djvuTextTree = $b;
+ }
+ else if ( $b->getName() == 'DjVuXML' ) {
+ $image->dejaMetaTree = $b;
+ }
+ }
+ } else {
+ $image->dejaMetaTree = $tree;
+ }
+ } catch( Exception $e ) {
+ wfDebug( "Bogus multipage XML metadata on '$image->name'\n" );
}
wfRestoreWarnings();
wfProfileOut( __METHOD__ );
- return $image->dejaMetaTree;
+ if( $gettext ) {
+ return $image->djvuTextTree;
+ } else {
+ return $image->dejaMetaTree;
+ }
}
function getImageSize( $image, $path ) {
@@ -211,4 +228,21 @@ class DjVuHandler extends ImageHandler {
return false;
}
}
+
+ function getPageText( $image, $page ){
+ $tree = $this->getMetaTree( $image, true );
+ if ( !$tree ) {
+ return false;
+ }
+
+ $o = $tree->BODY[0]->PAGE[$page-1];
+ if ( $o ) {
+ $txt = $o['value'];
+ return $txt;
+ } else {
+ return false;
+ }
+
+ }
+
}
diff --git a/includes/media/GIF.php b/includes/media/GIF.php
new file mode 100644
index 00000000..dbe5f813
--- /dev/null
+++ b/includes/media/GIF.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for GIF images.
+ *
+ * @ingroup Media
+ */
+class GIFHandler extends BitmapHandler {
+
+ function getMetadata( $image, $filename ) {
+ if ( !isset($image->parsedGIFMetadata) ) {
+ try {
+ $image->parsedGIFMetadata = GIFMetadataExtractor::getMetadata( $filename );
+ } catch( Exception $e ) {
+ // Broken file?
+ wfDebug( __METHOD__ . ': ' . $e->getMessage() . "\n" );
+ return '0';
+ }
+ }
+
+ return serialize($image->parsedGIFMetadata);
+
+ }
+
+ function formatMetadata( $image ) {
+ return false;
+ }
+
+ function getImageArea( $image, $width, $height ) {
+ $ser = $image->getMetadata();
+ if ($ser) {
+ $metadata = unserialize($ser);
+ return $width * $height * $metadata['frameCount'];
+ } else {
+ return $width * $height;
+ }
+ }
+
+ function getMetadataType( $image ) {
+ return 'parsed-gif';
+ }
+
+ function getLongDesc( $image ) {
+ global $wgUser, $wgLang;
+ $sk = $wgUser->getSkin();
+
+ $metadata = @unserialize($image->getMetadata());
+
+ if (!$metadata) return parent::getLongDesc( $image );
+
+ $info = array();
+ $info[] = $image->getMimeType();
+ $info[] = $sk->formatSize( $image->getSize() );
+
+ if ($metadata['looped'])
+ $info[] = wfMsgExt( 'file-info-gif-looped', 'parseinline' );
+
+ if ($metadata['frameCount'] > 1)
+ $info[] = wfMsgExt( 'file-info-gif-frames', 'parseinline', $metadata['frameCount'] );
+
+ if ($metadata['duration'])
+ $info[] = $wgLang->formatTimePeriod( $metadata['duration'] );
+
+ $infoString = $wgLang->commaList( $info );
+
+ return "($infoString)";
+ }
+}
diff --git a/includes/media/GIFMetadataExtractor.php b/includes/media/GIFMetadataExtractor.php
new file mode 100644
index 00000000..fac9012b
--- /dev/null
+++ b/includes/media/GIFMetadataExtractor.php
@@ -0,0 +1,175 @@
+<?php
+/**
+ * GIF frame counter.
+ * Originally written in Perl by Steve Sanbeg.
+ * Ported to PHP by Andrew Garrett
+ * Deliberately not using MWExceptions to avoid external dependencies, encouraging
+ * redistribution.
+ */
+
+class GIFMetadataExtractor {
+ static $gif_frame_sep;
+ static $gif_extension_sep;
+ static $gif_term;
+
+ static function getMetadata( $filename ) {
+ self::$gif_frame_sep = pack( "C", ord("," ) );
+ self::$gif_extension_sep = pack( "C", ord("!" ) );
+ self::$gif_term = pack( "C", ord(";" ) );
+
+ $frameCount = 0;
+ $duration = 0.0;
+ $isLooped = false;
+
+ if (!$filename)
+ throw new Exception( "No file name specified" );
+ elseif ( !file_exists($filename) || is_dir($filename) )
+ throw new Exception( "File $filename does not exist" );
+
+ $fh = fopen( $filename, 'r' );
+
+ if (!$fh)
+ throw new Exception( "Unable to open file $filename" );
+
+ // Check for the GIF header
+ $buf = fread( $fh, 6 );
+ if ( !($buf == 'GIF87a' || $buf == 'GIF89a') ) {
+ throw new Exception( "Not a valid GIF file; header: $buf" );
+ }
+
+ // Skip over width and height.
+ fread( $fh, 4 );
+
+ // Read BPP
+ $buf = fread( $fh, 1 );
+ $bpp = self::decodeBPP( $buf );
+
+ // Skip over background and aspect ratio
+ fread( $fh, 2 );
+
+ // Skip over the GCT
+ self::readGCT( $fh, $bpp );
+
+ while( !feof( $fh ) ) {
+ $buf = fread( $fh, 1 );
+
+ if ($buf == self::$gif_frame_sep) {
+ // Found a frame
+ $frameCount++;
+
+ ## Skip bounding box
+ fread( $fh, 8 );
+
+ ## Read BPP
+ $buf = fread( $fh, 1 );
+ $bpp = self::decodeBPP( $buf );
+
+ ## Read GCT
+ self::readGCT( $fh, $bpp );
+ fread( $fh, 1 );
+ self::skipBlock( $fh );
+ } elseif ( $buf == self::$gif_extension_sep ) {
+ $buf = fread( $fh, 1 );
+ $extension_code = unpack( 'C', $buf );
+ $extension_code = $extension_code[1];
+
+ if ($extension_code == 0xF9) {
+ // Graphics Control Extension.
+ fread( $fh, 1 ); // Block size
+
+ fread( $fh, 1 ); // Transparency, disposal method, user input
+
+ $buf = fread( $fh, 2 ); // Delay, in hundredths of seconds.
+ $delay = unpack( 'v', $buf );
+ $delay = $delay[1];
+ $duration += $delay * 0.01;
+
+ fread( $fh, 1 ); // Transparent colour index
+
+ $term = fread( $fh, 1 ); // Should be a terminator
+ $term = unpack( 'C', $term );
+ $term = $term[1];
+ if ($term != 0 )
+ throw new Exception( "Malformed Graphics Control Extension block" );
+ } elseif ($extension_code == 0xFF) {
+ // Application extension (Netscape info about the animated gif)
+ $blockLength = fread( $fh, 1 );
+ $blockLength = unpack( 'C', $blockLength );
+ $blockLength = $blockLength[1];
+ $data = fread( $fh, $blockLength );
+
+ // NETSCAPE2.0 (application name)
+ if ($blockLength != 11 || $data != 'NETSCAPE2.0') {
+ fseek( $fh, -($blockLength + 1), SEEK_CUR );
+ self::skipBlock( $fh );
+ continue;
+ }
+
+ $data = fread( $fh, 2 ); // Block length and introduction, should be 03 01
+
+ if ($data != "\x03\x01") {
+ throw new Exception( "Expected \x03\x01, got $data" );
+ }
+
+ // Unsigned little-endian integer, loop count or zero for "forever"
+ $loopData = fread( $fh, 2 );
+ $loopData = unpack( 'v', $loopData );
+ $loopCount = $loopData[1];
+
+ if ($loopCount != 1) {
+ $isLooped = true;
+ }
+
+ // Read out terminator byte
+ fread( $fh, 1 );
+ } else {
+ self::skipBlock( $fh );
+ }
+ } elseif ( $buf == self::$gif_term ) {
+ break;
+ } else {
+ $byte = unpack( 'C', $buf );
+ $byte = $byte[1];
+ throw new Exception( "At position: ".ftell($fh). ", Unknown byte ".$byte );
+ }
+ }
+
+ return array(
+ 'frameCount' => $frameCount,
+ 'looped' => $isLooped,
+ 'duration' => $duration
+ );
+
+ }
+
+ static function readGCT( $fh, $bpp ) {
+ if ($bpp > 0) {
+ for( $i=1; $i<=pow(2,$bpp); ++$i ) {
+ fread( $fh, 3 );
+ }
+ }
+ }
+
+ static function decodeBPP( $data ) {
+ $buf = unpack( 'C', $data );
+ $buf = $buf[1];
+ $bpp = ( $buf & 7 ) + 1;
+ $buf >>= 7;
+
+ $have_map = $buf & 1;
+
+ return $have_map ? $bpp : 0;
+ }
+
+ static function skipBlock( $fh ) {
+ while ( !feof( $fh ) ) {
+ $buf = fread( $fh, 1 );
+ $block_len = unpack( 'C', $buf );
+ $block_len = $block_len[1];
+ if ($block_len == 0)
+ return;
+ fread( $fh, $block_len );
+ }
+ }
+
+}
diff --git a/includes/media/Generic.php b/includes/media/Generic.php
index a9c681e1..8a4d7054 100644
--- a/includes/media/Generic.php
+++ b/includes/media/Generic.php
@@ -71,18 +71,18 @@ abstract class MediaHandler {
* 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
- * @param string $fileName The filename
- * @return array
+ * @param $image File: the image object, or false if there isn't one
+ * @param $fileName String: the filename
+ * @return Array
*/
abstract function getImageSize( $image, $path );
/**
* Get handler-specific metadata which will be saved in the img_metadata field.
*
- * @param Image $image The image object, or false if there isn't one
- * @param string $fileName The filename
- * @return string
+ * @param $image File: the image object, or false if there isn't one
+ * @param $path String: the filename
+ * @return String
*/
function getMetadata( $image, $path ) { return ''; }
@@ -114,10 +114,10 @@ abstract class MediaHandler {
* Get a MediaTransformOutput object representing the transformed output. Does not
* actually do the transform.
*
- * @param Image $image The image object
- * @param string $dstPath Filesystem destination path
- * @param string $dstUrl Destination URL to use in output HTML
- * @param array $params Arbitrary set of parameters validated by $this->validateParam()
+ * @param $image File: the image object
+ * @param $dstPath String: filesystem destination path
+ * @param $dstUrl String: Destination URL to use in output HTML
+ * @param $params Array: Arbitrary set of parameters validated by $this->validateParam()
*/
function getTransform( $image, $dstPath, $dstUrl, $params ) {
return $this->doTransform( $image, $dstPath, $dstUrl, $params, self::TRANSFORM_LATER );
@@ -127,11 +127,11 @@ abstract class MediaHandler {
* Get a MediaTransformOutput object representing the transformed output. Does the
* transform unless $flags contains self::TRANSFORM_LATER.
*
- * @param Image $image The image object
- * @param string $dstPath Filesystem destination path
- * @param string $dstUrl Destination URL to use in output HTML
- * @param array $params Arbitrary set of parameters validated by $this->validateParam()
- * @param integer $flags A bitfield, may contain self::TRANSFORM_LATER
+ * @param $image File: the image object
+ * @param $dstPath String: filesystem destination path
+ * @param $dstUrl String: destination URL to use in output HTML
+ * @param $params Array: arbitrary set of parameters validated by $this->validateParam()
+ * @param $flags Integer: a bitfield, may contain self::TRANSFORM_LATER
*/
abstract function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 );
@@ -180,6 +180,14 @@ abstract class MediaHandler {
}
/**
+ * Generic getter for text layer.
+ * Currently overloaded by PDF and DjVu handlers
+ */
+ function getPageText( $image, $page ) {
+ return false;
+ }
+
+ /**
* Get an array structure that looks like this:
*
* array(
@@ -210,7 +218,7 @@ abstract class MediaHandler {
}
/**
- * @fixme document this!
+ * @todo Fixme: document this!
* 'value' thingy goes into a wikitext table; it used to be escaped but
* that was incompatible with previous practice of customized display
* with wikitext formatting via messages such as 'exif-model-value'.
@@ -376,8 +384,8 @@ abstract class ImageHandler extends MediaHandler {
/**
* Validate thumbnail parameters and fill in the correct height
*
- * @param integer &$width Specified width (input/output)
- * @param integer &$height Height (output only)
+ * @param $width Integer: specified width (input/output)
+ * @param $height Integer: height (output only)
* @return false to indicate that an error should be returned to the user.
*/
function validateThumbParams( &$width, &$height, $srcWidth, $srcHeight, $mimeType ) {
diff --git a/includes/media/SVG.php b/includes/media/SVG.php
index f0519e89..4cc66fb1 100644
--- a/includes/media/SVG.php
+++ b/includes/media/SVG.php
@@ -40,8 +40,6 @@ class SvgHandler extends ImageHandler {
}
function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- global $wgSVGConverters, $wgSVGConverter, $wgSVGConverterPath;
-
if ( !$this->normaliseParams( $image, $params ) ) {
return new TransformParameterError( $params );
}
diff --git a/includes/memcached-client.php b/includes/memcached-client.php
index 79745309..3b0ae90d 100644
--- a/includes/memcached-client.php
+++ b/includes/memcached-client.php
@@ -44,7 +44,7 @@
*
* require_once 'memcached.php';
*
- * $mc = new memcached(array(
+ * $mc = new MWMemcached(array(
* 'servers' => array('127.0.0.1:10000',
* array('192.0.0.1:10010', 2),
* '127.0.0.1:10020'),
@@ -63,1027 +63,989 @@
// {{{ requirements
// }}}
-// {{{ class memcached
+// {{{ class MWMemcached
/**
* memcached client class implemented using (p)fsockopen()
*
* @author Ryan T. Dean <rtdean@cytherianage.net>
* @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
- *
- * @var array
- * @access public
- */
- var $stats;
-
- // }}}
- // {{{ private
-
- /**
- * Cached Sockets that are connected
- *
- * @var array
- * @access private
- */
- var $_cache_sock;
-
- /**
- * Current debug status; 0 - none to 9 - profiling
- *
- * @var boolean
- * @access private
- */
- var $_debug;
-
- /**
- * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
- *
- * @var array
- * @access private
- */
- var $_host_dead;
-
- /**
- * Is compression available?
- *
- * @var boolean
- * @access private
- */
- var $_have_zlib;
-
- /**
- * Do we want to use compression?
- *
- * @var boolean
- * @access private
- */
- var $_compress_enable;
-
- /**
- * At how many bytes should we compress?
- *
- * @var integer
- * @access private
- */
- var $_compress_threshold;
-
- /**
- * Are we using persistant links?
- *
- * @var boolean
- * @access private
- */
- var $_persistant;
-
- /**
- * If only using one server; contains ip:port to connect to
- *
- * @var string
- * @access private
- */
- var $_single_sock;
-
- /**
- * Array containing ip:port or array(ip:port, weight)
- *
- * @var array
- * @access private
- */
- var $_servers;
-
- /**
- * Our bit buckets
- *
- * @var array
- * @access private
- */
- var $_buckets;
-
- /**
- * Total # of bit buckets we have
- *
- * @var integer
- * @access private
- */
- var $_bucketcount;
-
- /**
- * # of total servers we have
- *
- * @var integer
- * @access private
- */
- var $_active;
-
- /**
- * Stream timeout in seconds. Applies for example to fread()
- *
- * @var integer
- * @access private
- */
- var $_timeout_seconds;
-
- /**
- * Stream timeout in microseconds
- *
- * @var integer
- * @access private
- */
- var $_timeout_microseconds;
-
- /**
- * Connect timeout in seconds
- */
- var $_connect_timeout;
-
- /**
- * Number of connection attempts for each server
- */
- var $_connect_attempts;
-
- // }}}
- // }}}
- // {{{ methods
- // {{{ public functions
- // {{{ memcached()
-
- /**
- * Memcache initializer
- *
- * @param array $args Associative array of settings
- *
- * @return mixed
- * @access public
- */
- function memcached ($args)
- {
- $this->set_servers(@$args['servers']);
- $this->_debug = @$args['debug'];
- $this->stats = array();
- $this->_compress_threshold = @$args['compress_threshold'];
- $this->_persistant = array_key_exists('persistant', $args) ? (@$args['persistant']) : false;
- $this->_compress_enable = true;
- $this->_have_zlib = function_exists("gzcompress");
-
- $this->_cache_sock = array();
- $this->_host_dead = array();
-
- $this->_timeout_seconds = 1;
- $this->_timeout_microseconds = 0;
-
- $this->_connect_timeout = 0.01;
- $this->_connect_attempts = 3;
- }
-
- // }}}
- // {{{ add()
-
- /**
- * Adds a key/value to the memcache server if one isn't already set with
- * that key
- *
- * @param string $key Key to set with data
- * @param mixed $val Value to store
- * @param integer $exp (optional) Time to expire data at
- *
- * @return boolean
- * @access public
- */
- function add ($key, $val, $exp = 0)
- {
- return $this->_set('add', $key, $val, $exp);
- }
-
- // }}}
- // {{{ decr()
-
- /**
- * Decriment a value stored on the memcache server
- *
- * @param string $key Key to decriment
- * @param integer $amt (optional) Amount to decriment
- *
- * @return mixed FALSE on failure, value on success
- * @access public
- */
- function decr ($key, $amt=1)
- {
- return $this->_incrdecr('decr', $key, $amt);
- }
-
- // }}}
- // {{{ delete()
-
- /**
- * Deletes a key from the server, optionally after $time
- *
- * @param string $key Key to delete
- * @param integer $time (optional) How long to wait before deleting
- *
- * @return boolean TRUE on success, FALSE on failure
- * @access public
- */
- function delete ($key, $time = 0)
- {
- if (!$this->_active)
- return false;
-
- $sock = $this->get_sock($key);
- if (!is_resource($sock))
- return false;
-
- $key = is_array($key) ? $key[1] : $key;
-
- @$this->stats['delete']++;
- $cmd = "delete $key $time\r\n";
- if(!$this->_safe_fwrite($sock, $cmd, strlen($cmd)))
- {
- $this->_dead_sock($sock);
- return false;
- }
- $res = trim(fgets($sock));
-
- if ($this->_debug)
- $this->_debugprint(sprintf("MemCache: delete %s (%s)\n", $key, $res));
-
- if ($res == "DELETED")
- return true;
- return false;
- }
-
- // }}}
- // {{{ disconnect_all()
-
- /**
- * Disconnects all connected sockets
- *
- * @access public
- */
- function disconnect_all ()
- {
- foreach ($this->_cache_sock as $sock)
- fclose($sock);
-
- $this->_cache_sock = array();
- }
-
- // }}}
- // {{{ enable_compress()
-
- /**
- * Enable / Disable compression
- *
- * @param boolean $enable TRUE to enable, FALSE to disable
- *
- * @access public
- */
- function enable_compress ($enable)
- {
- $this->_compress_enable = $enable;
- }
-
- // }}}
- // {{{ forget_dead_hosts()
-
- /**
- * Forget about all of the dead hosts
- *
- * @access public
- */
- function forget_dead_hosts ()
- {
- $this->_host_dead = array();
- }
-
- // }}}
- // {{{ get()
-
- /**
- * Retrieves the value associated with the key from the memcache server
- *
- * @param string $key Key to retrieve
- *
- * @return mixed
- * @access public
- */
- function get ($key)
- {
- $fname = 'memcached::get';
- wfProfileIn( $fname );
-
- if ( $this->_debug ) {
- $this->_debugprint( "get($key)\n" );
- }
-
- if (!$this->_active) {
- wfProfileOut( $fname );
- return false;
- }
-
- $sock = $this->get_sock($key);
-
- if (!is_resource($sock)) {
- wfProfileOut( $fname );
- return false;
- }
-
- @$this->stats['get']++;
-
- $cmd = "get $key\r\n";
- if (!$this->_safe_fwrite($sock, $cmd, strlen($cmd)))
- {
- $this->_dead_sock($sock);
- wfProfileOut( $fname );
- return false;
- }
-
- $val = array();
- $this->_load_items($sock, $val);
-
- if ($this->_debug)
- foreach ($val as $k => $v)
- $this->_debugprint(sprintf("MemCache: sock %s got %s\n", serialize($sock), $k));
-
- wfProfileOut( $fname );
- return @$val[$key];
- }
-
- // }}}
- // {{{ get_multi()
-
- /**
- * Get multiple keys from the server(s)
- *
- * @param array $keys Keys to retrieve
- *
- * @return array
- * @access public
- */
- function get_multi ($keys)
- {
- if (!$this->_active)
- return false;
-
- @$this->stats['get_multi']++;
- $sock_keys = array();
-
- foreach ($keys as $key)
- {
- $sock = $this->get_sock($key);
- if (!is_resource($sock)) continue;
- $key = is_array($key) ? $key[1] : $key;
- if (!isset($sock_keys[$sock]))
- {
- $sock_keys[$sock] = array();
- $socks[] = $sock;
- }
- $sock_keys[$sock][] = $key;
- }
-
- // Send out the requests
- foreach ($socks as $sock)
- {
- $cmd = "get";
- foreach ($sock_keys[$sock] as $key)
- {
- $cmd .= " ". $key;
- }
- $cmd .= "\r\n";
-
- if ($this->_safe_fwrite($sock, $cmd, strlen($cmd)))
- {
- $gather[] = $sock;
- } else
- {
- $this->_dead_sock($sock);
- }
- }
-
- // Parse responses
- $val = array();
- foreach ($gather as $sock)
- {
- $this->_load_items($sock, $val);
- }
-
- if ($this->_debug)
- foreach ($val as $k => $v)
- $this->_debugprint(sprintf("MemCache: got %s\n", $k));
-
- return $val;
- }
-
- // }}}
- // {{{ incr()
-
- /**
- * Increments $key (optionally) by $amt
- *
- * @param string $key Key to increment
- * @param integer $amt (optional) amount to increment
- *
- * @return integer New key value?
- * @access public
- */
- function incr ($key, $amt=1)
- {
- return $this->_incrdecr('incr', $key, $amt);
- }
-
- // }}}
- // {{{ replace()
-
- /**
- * Overwrites an existing value for key; only works if key is already set
- *
- * @param string $key Key to set value as
- * @param mixed $value Value to store
- * @param integer $exp (optional) Experiation time
- *
- * @return boolean
- * @access public
- */
- function replace ($key, $value, $exp=0)
- {
- return $this->_set('replace', $key, $value, $exp);
- }
-
- // }}}
- // {{{ run_command()
-
- /**
- * Passes through $cmd to the memcache server connected by $sock; returns
- * output as an array (null array if no output)
- *
- * NOTE: due to a possible bug in how PHP reads while using fgets(), each
- * line may not be terminated by a \r\n. More specifically, my testing
- * has shown that, on FreeBSD at least, each line is terminated only
- * with a \n. This is with the PHP flag auto_detect_line_endings set
- * to falase (the default).
- *
- * @param resource $sock Socket to send command on
- * @param string $cmd Command to run
- *
- * @return array Output array
- * @access public
- */
- function run_command ($sock, $cmd)
- {
- if (!is_resource($sock))
- return array();
-
- if (!$this->_safe_fwrite($sock, $cmd, strlen($cmd)))
- return array();
-
- while (true)
- {
- $res = fgets($sock);
- $ret[] = $res;
- if (preg_match('/^END/', $res))
- break;
- if (strlen($res) == 0)
- break;
- }
- return $ret;
- }
-
- // }}}
- // {{{ set()
-
- /**
- * Unconditionally sets a key to a given value in the memcache. Returns true
- * if set successfully.
- *
- * @param string $key Key to set value as
- * @param mixed $value Value to set
- * @param integer $exp (optional) Experiation time
- *
- * @return boolean TRUE on success
- * @access public
- */
- function set ($key, $value, $exp=0)
- {
- return $this->_set('set', $key, $value, $exp);
- }
-
- // }}}
- // {{{ set_compress_threshold()
-
- /**
- * Sets the compression threshold
- *
- * @param integer $thresh Threshold to compress if larger than
- *
- * @access public
- */
- function set_compress_threshold ($thresh)
- {
- $this->_compress_threshold = $thresh;
- }
-
- // }}}
- // {{{ set_debug()
-
- /**
- * Sets the debug flag
- *
- * @param boolean $dbg TRUE for debugging, FALSE otherwise
- *
- * @access public
- *
- * @see memcahced::memcached
- */
- function set_debug ($dbg)
- {
- $this->_debug = $dbg;
- }
-
- // }}}
- // {{{ set_servers()
-
- /**
- * Sets the server list to distribute key gets and puts between
- *
- * @param array $list Array of servers to connect to
- *
- * @access public
- *
- * @see memcached::memcached()
- */
- function set_servers ($list)
- {
- $this->_servers = $list;
- $this->_active = count($list);
- $this->_buckets = null;
- $this->_bucketcount = 0;
-
- $this->_single_sock = null;
- if ($this->_active == 1)
- $this->_single_sock = $this->_servers[0];
- }
-
- /**
- * Sets the timeout for new connections
- *
- * @param integer $seconds Number of seconds
- * @param integer $microseconds Number of microseconds
- *
- * @access public
- */
- function set_timeout ($seconds, $microseconds)
- {
- $this->_timeout_seconds = $seconds;
- $this->_timeout_microseconds = $microseconds;
- }
-
- // }}}
- // }}}
- // {{{ private methods
- // {{{ _close_sock()
-
- /**
- * Close the specified socket
- *
- * @param string $sock Socket to close
- *
- * @access private
- */
- function _close_sock ($sock)
- {
- $host = array_search($sock, $this->_cache_sock);
- fclose($this->_cache_sock[$host]);
- unset($this->_cache_sock[$host]);
- }
-
- // }}}
- // {{{ _connect_sock()
-
- /**
- * Connects $sock to $host, timing out after $timeout
- *
- * @param integer $sock Socket to connect
- * @param string $host Host:IP to connect to
- *
- * @return boolean
- * @access private
- */
- function _connect_sock (&$sock, $host)
- {
- list ($ip, $port) = explode(":", $host);
- $sock = false;
- $timeout = $this->_connect_timeout;
- $errno = $errstr = null;
- for ($i = 0; !$sock && $i < $this->_connect_attempts; $i++) {
- if ($i > 0) {
- # Sleep until the timeout, in case it failed fast
- $elapsed = microtime(true) - $t;
- if ( $elapsed < $timeout ) {
- usleep(($timeout - $elapsed) * 1e6);
- }
- $timeout *= 2;
- }
- $t = microtime(true);
- if ($this->_persistant == 1)
- {
- $sock = @pfsockopen($ip, $port, $errno, $errstr, $timeout);
- } else
- {
- $sock = @fsockopen($ip, $port, $errno, $errstr, $timeout);
- }
- }
- if (!$sock) {
- if ($this->_debug)
- $this->_debugprint( "Error connecting to $host: $errstr\n" );
- return false;
- }
-
- // Initialise timeout
- stream_set_timeout($sock, $this->_timeout_seconds, $this->_timeout_microseconds);
-
- return true;
- }
-
- // }}}
- // {{{ _dead_sock()
-
- /**
- * Marks a host as dead until 30-40 seconds in the future
- *
- * @param string $sock Socket to mark as dead
- *
- * @access private
- */
- function _dead_sock ($sock)
- {
- $host = array_search($sock, $this->_cache_sock);
- @list ($ip, /* $port */) = explode(":", $host);
- $this->_host_dead[$ip] = time() + 30 + intval(rand(0, 10));
- $this->_host_dead[$host] = $this->_host_dead[$ip];
- unset($this->_cache_sock[$host]);
- }
-
- // }}}
- // {{{ get_sock()
-
- /**
- * get_sock
- *
- * @param string $key Key to retrieve value for;
- *
- * @return mixed resource on success, false on failure
- * @access private
- */
- function get_sock ($key)
- {
- if (!$this->_active)
- return false;
-
- if ($this->_single_sock !== null) {
- $this->_flush_read_buffer($this->_single_sock);
- return $this->sock_to_host($this->_single_sock);
- }
-
- $hv = is_array($key) ? intval($key[0]) : $this->_hashfunc($key);
-
- if ($this->_buckets === null)
- {
- foreach ($this->_servers as $v)
- {
- if (is_array($v))
- {
- for ($i=0; $i<$v[1]; $i++)
- $bu[] = $v[0];
- } else
- {
- $bu[] = $v;
- }
- }
- $this->_buckets = $bu;
- $this->_bucketcount = count($bu);
- }
-
- $realkey = is_array($key) ? $key[1] : $key;
- for ($tries = 0; $tries<20; $tries++)
- {
- $host = $this->_buckets[$hv % $this->_bucketcount];
- $sock = $this->sock_to_host($host);
- if (is_resource($sock)) {
- $this->_flush_read_buffer($sock);
- return $sock;
- }
- $hv = $this->_hashfunc( $hv . $realkey );
- }
-
- return false;
- }
-
- // }}}
- // {{{ _hashfunc()
-
- /**
- * Creates a hash integer based on the $key
- *
- * @param string $key Key to hash
- *
- * @return integer Hash value
- * @access private
- */
- function _hashfunc ($key)
- {
- # Hash function must on [0,0x7ffffff]
- # We take the first 31 bits of the MD5 hash, which unlike the hash
- # function used in a previous version of this client, works
- return hexdec(substr(md5($key),0,8)) & 0x7fffffff;
- }
-
- // }}}
- // {{{ _incrdecr()
-
- /**
- * Perform increment/decriment on $key
- *
- * @param string $cmd Command to perform
- * @param string $key Key to perform it on
- * @param integer $amt Amount to adjust
- *
- * @return integer New value of $key
- * @access private
- */
- function _incrdecr ($cmd, $key, $amt=1)
- {
- if (!$this->_active)
- return null;
-
- $sock = $this->get_sock($key);
- if (!is_resource($sock))
- return null;
-
- $key = is_array($key) ? $key[1] : $key;
- @$this->stats[$cmd]++;
- if (!$this->_safe_fwrite($sock, "$cmd $key $amt\r\n"))
- return $this->_dead_sock($sock);
-
- stream_set_timeout($sock, 1, 0);
- $line = fgets($sock);
- $match = array();
- if (!preg_match('/^(\d+)/', $line, $match))
- return null;
- return $match[1];
- }
-
- // }}}
- // {{{ _load_items()
-
- /**
- * Load items into $ret from $sock
- *
- * @param resource $sock Socket to read from
- * @param array $ret Returned values
- *
- * @access private
- */
- function _load_items ($sock, &$ret)
- {
- while (1)
- {
- $decl = fgets($sock);
- if ($decl == "END\r\n")
- {
- return true;
- } elseif (preg_match('/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match))
- {
- list($rkey, $flags, $len) = array($match[1], $match[2], $match[3]);
- $bneed = $len+2;
- $offset = 0;
-
- while ($bneed > 0)
- {
- $data = fread($sock, $bneed);
- $n = strlen($data);
- if ($n == 0)
- break;
- $offset += $n;
- $bneed -= $n;
- @$ret[$rkey] .= $data;
- }
-
- if ($offset != $len+2)
- {
- // Something is borked!
- if ($this->_debug)
- $this->_debugprint(sprintf("Something is borked! key %s expecting %d got %d length\n", $rkey, $len+2, $offset));
-
- unset($ret[$rkey]);
- $this->_close_sock($sock);
- return false;
- }
-
- if ($this->_have_zlib && $flags & memcached::COMPRESSED)
- $ret[$rkey] = gzuncompress($ret[$rkey]);
-
- $ret[$rkey] = rtrim($ret[$rkey]);
-
- if ($flags & memcached::SERIALIZED)
- $ret[$rkey] = unserialize($ret[$rkey]);
-
- } else
- {
- $this->_debugprint("Error parsing memcached response\n");
- return 0;
- }
- }
- }
-
- // }}}
- // {{{ _set()
-
- /**
- * Performs the requested storage operation to the memcache server
- *
- * @param string $cmd Command to perform
- * @param string $key Key to act on
- * @param mixed $val What we need to store
- * @param integer $exp When it should expire
- *
- * @return boolean
- * @access private
- */
- function _set ($cmd, $key, $val, $exp)
- {
- if (!$this->_active)
- return false;
-
- $sock = $this->get_sock($key);
- if (!is_resource($sock))
- return false;
-
- @$this->stats[$cmd]++;
-
- $flags = 0;
-
- if (!is_scalar($val))
- {
- $val = serialize($val);
- $flags |= memcached::SERIALIZED;
- if ($this->_debug)
- $this->_debugprint(sprintf("client: serializing data as it is not scalar\n"));
- }
-
- $len = strlen($val);
-
- if ($this->_have_zlib && $this->_compress_enable &&
- $this->_compress_threshold && $len >= $this->_compress_threshold)
- {
- $c_val = gzcompress($val, 9);
- $c_len = strlen($c_val);
-
- 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 |= memcached::COMPRESSED;
- }
- }
- if (!$this->_safe_fwrite($sock, "$cmd $key $flags $exp $len\r\n$val\r\n"))
- return $this->_dead_sock($sock);
-
- $line = trim(fgets($sock));
-
- if ($this->_debug)
- {
- $this->_debugprint(sprintf("%s %s (%s)\n", $cmd, $key, $line));
- }
- if ($line == "STORED")
- return true;
- return false;
- }
-
- // }}}
- // {{{ sock_to_host()
-
- /**
- * Returns the socket for the host
- *
- * @param string $host Host:IP to get socket for
- *
- * @return mixed IO Stream or false
- * @access private
- */
- function sock_to_host ($host)
- {
- if (isset($this->_cache_sock[$host]))
- return $this->_cache_sock[$host];
-
- $sock = null;
- $now = time();
- list ($ip, /* $port */) = explode (":", $host);
- if (isset($this->_host_dead[$host]) && $this->_host_dead[$host] > $now ||
- isset($this->_host_dead[$ip]) && $this->_host_dead[$ip] > $now)
- return null;
-
- if (!$this->_connect_sock($sock, $host))
- return $this->_dead_sock($host);
-
- // Do not buffer writes
- stream_set_write_buffer($sock, 0);
-
- $this->_cache_sock[$host] = $sock;
-
- return $this->_cache_sock[$host];
- }
-
- function _debugprint($str){
- print($str);
- }
-
- /**
- * Write to a stream, timing out after the correct amount of time
- *
- * @return bool false on failure, true on success
- */
- /*
- function _safe_fwrite($f, $buf, $len = false) {
- stream_set_blocking($f, 0);
-
- if ($len === false) {
- wfDebug("Writing " . strlen( $buf ) . " bytes\n");
- $bytesWritten = fwrite($f, $buf);
- } else {
- wfDebug("Writing $len bytes\n");
- $bytesWritten = fwrite($f, $buf, $len);
- }
- $n = stream_select($r=NULL, $w = array($f), $e = NULL, 10, 0);
- # $this->_timeout_seconds, $this->_timeout_microseconds);
-
- wfDebug("stream_select returned $n\n");
- stream_set_blocking($f, 1);
- return $n == 1;
- return $bytesWritten;
- }*/
-
- /**
- * Original behaviour
- */
- function _safe_fwrite($f, $buf, $len = false) {
- if ($len === false) {
- $bytesWritten = fwrite($f, $buf);
- } else {
- $bytesWritten = fwrite($f, $buf, $len);
- }
- return $bytesWritten;
- }
-
- /**
- * Flush the read buffer of a stream
- */
- function _flush_read_buffer($f) {
- if (!is_resource($f)) {
- return;
- }
- $n = stream_select($r=array($f), $w = NULL, $e = NULL, 0, 0);
- while ($n == 1 && !feof($f)) {
- fread($f, 1024);
- $n = stream_select($r=array($f), $w = NULL, $e = NULL, 0, 0);
- }
- }
-
- // }}}
- // }}}
- // }}}
+class MWMemcached {
+ // {{{ 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
+ *
+ * @var array
+ * @access public
+ */
+ var $stats;
+
+ // }}}
+ // {{{ private
+
+ /**
+ * Cached Sockets that are connected
+ *
+ * @var array
+ * @access private
+ */
+ var $_cache_sock;
+
+ /**
+ * Current debug status; 0 - none to 9 - profiling
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_debug;
+
+ /**
+ * Dead hosts, assoc array, 'host'=>'unixtime when ok to check again'
+ *
+ * @var array
+ * @access private
+ */
+ var $_host_dead;
+
+ /**
+ * Is compression available?
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_have_zlib;
+
+ /**
+ * Do we want to use compression?
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_compress_enable;
+
+ /**
+ * At how many bytes should we compress?
+ *
+ * @var integer
+ * @access private
+ */
+ var $_compress_threshold;
+
+ /**
+ * Are we using persistant links?
+ *
+ * @var boolean
+ * @access private
+ */
+ var $_persistant;
+
+ /**
+ * If only using one server; contains ip:port to connect to
+ *
+ * @var string
+ * @access private
+ */
+ var $_single_sock;
+
+ /**
+ * Array containing ip:port or array(ip:port, weight)
+ *
+ * @var array
+ * @access private
+ */
+ var $_servers;
+
+ /**
+ * Our bit buckets
+ *
+ * @var array
+ * @access private
+ */
+ var $_buckets;
+
+ /**
+ * Total # of bit buckets we have
+ *
+ * @var integer
+ * @access private
+ */
+ var $_bucketcount;
+
+ /**
+ * # of total servers we have
+ *
+ * @var integer
+ * @access private
+ */
+ var $_active;
+
+ /**
+ * Stream timeout in seconds. Applies for example to fread()
+ *
+ * @var integer
+ * @access private
+ */
+ var $_timeout_seconds;
+
+ /**
+ * Stream timeout in microseconds
+ *
+ * @var integer
+ * @access private
+ */
+ var $_timeout_microseconds;
+
+ /**
+ * Connect timeout in seconds
+ */
+ var $_connect_timeout;
+
+ /**
+ * Number of connection attempts for each server
+ */
+ var $_connect_attempts;
+
+ // }}}
+ // }}}
+ // {{{ methods
+ // {{{ public functions
+ // {{{ memcached()
+
+ /**
+ * Memcache initializer
+ *
+ * @param array $args Associative array of settings
+ *
+ * @return mixed
+ */
+ public function __construct( $args ) {
+ global $wgMemCachedTimeout;
+ $this->set_servers( @$args['servers'] );
+ $this->_debug = @$args['debug'];
+ $this->stats = array();
+ $this->_compress_threshold = @$args['compress_threshold'];
+ $this->_persistant = array_key_exists( 'persistant', $args ) ? ( @$args['persistant'] ) : false;
+ $this->_compress_enable = true;
+ $this->_have_zlib = function_exists( 'gzcompress' );
+
+ $this->_cache_sock = array();
+ $this->_host_dead = array();
+
+ $this->_timeout_seconds = 0;
+ $this->_timeout_microseconds = $wgMemCachedTimeout;
+
+ $this->_connect_timeout = 0.01;
+ $this->_connect_attempts = 2;
+ }
+
+ // }}}
+ // {{{ add()
+
+ /**
+ * Adds a key/value to the memcache server if one isn't already set with
+ * that key
+ *
+ * @param string $key Key to set with data
+ * @param mixed $val Value to store
+ * @param integer $exp (optional) Time to expire data at
+ *
+ * @return boolean
+ */
+ public function add( $key, $val, $exp = 0 ) {
+ return $this->_set( 'add', $key, $val, $exp );
+ }
+
+ // }}}
+ // {{{ decr()
+
+ /**
+ * Decriment a value stored on the memcache server
+ *
+ * @param string $key Key to decriment
+ * @param integer $amt (optional) Amount to decriment
+ *
+ * @return mixed FALSE on failure, value on success
+ */
+ public function decr( $key, $amt = 1 ) {
+ return $this->_incrdecr( 'decr', $key, $amt );
+ }
+
+ // }}}
+ // {{{ delete()
+
+ /**
+ * Deletes a key from the server, optionally after $time
+ *
+ * @param string $key Key to delete
+ * @param integer $time (optional) How long to wait before deleting
+ *
+ * @return boolean TRUE on success, FALSE on failure
+ */
+ public function delete( $key, $time = 0 ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return false;
+ }
+
+ $key = is_array( $key ) ? $key[1] : $key;
+
+ @$this->stats['delete']++;
+ $cmd = "delete $key $time\r\n";
+ if( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
+ $this->_dead_sock( $sock );
+ return false;
+ }
+ $res = trim( fgets( $sock ) );
+
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "MemCache: delete %s (%s)\n", $key, $res ) );
+ }
+
+ if ( $res == "DELETED" ) {
+ return true;
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ disconnect_all()
+
+ /**
+ * Disconnects all connected sockets
+ */
+ public function disconnect_all() {
+ foreach ( $this->_cache_sock as $sock ) {
+ fclose( $sock );
+ }
+
+ $this->_cache_sock = array();
+ }
+
+ // }}}
+ // {{{ enable_compress()
+
+ /**
+ * Enable / Disable compression
+ *
+ * @param boolean $enable TRUE to enable, FALSE to disable
+ */
+ public function enable_compress( $enable ) {
+ $this->_compress_enable = $enable;
+ }
+
+ // }}}
+ // {{{ forget_dead_hosts()
+
+ /**
+ * Forget about all of the dead hosts
+ */
+ public function forget_dead_hosts() {
+ $this->_host_dead = array();
+ }
+
+ // }}}
+ // {{{ get()
+
+ /**
+ * Retrieves the value associated with the key from the memcache server
+ *
+ * @param string $key Key to retrieve
+ *
+ * @return mixed
+ */
+ public function get( $key ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( $this->_debug ) {
+ $this->_debugprint( "get($key)\n" );
+ }
+
+ if ( !$this->_active ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+
+ if ( !is_resource( $sock ) ) {
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ @$this->stats['get']++;
+
+ $cmd = "get $key\r\n";
+ if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
+ $this->_dead_sock( $sock );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ $val = array();
+ $this->_load_items( $sock, $val );
+
+ if ( $this->_debug ) {
+ foreach ( $val as $k => $v ) {
+ $this->_debugprint( sprintf( "MemCache: sock %s got %s\n", serialize( $sock ), $k ) );
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ return @$val[$key];
+ }
+
+ // }}}
+ // {{{ get_multi()
+
+ /**
+ * Get multiple keys from the server(s)
+ *
+ * @param array $keys Keys to retrieve
+ *
+ * @return array
+ */
+ public function get_multi( $keys ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ @$this->stats['get_multi']++;
+ $sock_keys = array();
+
+ foreach ( $keys as $key ) {
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ continue;
+ }
+ $key = is_array( $key ) ? $key[1] : $key;
+ if ( !isset( $sock_keys[$sock] ) ) {
+ $sock_keys[$sock] = array();
+ $socks[] = $sock;
+ }
+ $sock_keys[$sock][] = $key;
+ }
+
+ // Send out the requests
+ foreach ( $socks as $sock ) {
+ $cmd = 'get';
+ foreach ( $sock_keys[$sock] as $key ) {
+ $cmd .= ' ' . $key;
+ }
+ $cmd .= "\r\n";
+
+ if ( $this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
+ $gather[] = $sock;
+ } else {
+ $this->_dead_sock( $sock );
+ }
+ }
+
+ // Parse responses
+ $val = array();
+ foreach ( $gather as $sock ) {
+ $this->_load_items( $sock, $val );
+ }
+
+ if ( $this->_debug ) {
+ foreach ( $val as $k => $v ) {
+ $this->_debugprint( sprintf( "MemCache: got %s\n", $k ) );
+ }
+ }
+
+ return $val;
+ }
+
+ // }}}
+ // {{{ incr()
+
+ /**
+ * Increments $key (optionally) by $amt
+ *
+ * @param string $key Key to increment
+ * @param integer $amt (optional) amount to increment
+ *
+ * @return integer New key value?
+ */
+ public function incr( $key, $amt = 1 ) {
+ return $this->_incrdecr( 'incr', $key, $amt );
+ }
+
+ // }}}
+ // {{{ replace()
+
+ /**
+ * Overwrites an existing value for key; only works if key is already set
+ *
+ * @param string $key Key to set value as
+ * @param mixed $value Value to store
+ * @param integer $exp (optional) Experiation time
+ *
+ * @return boolean
+ */
+ public function replace( $key, $value, $exp = 0 ) {
+ return $this->_set( 'replace', $key, $value, $exp );
+ }
+
+ // }}}
+ // {{{ run_command()
+
+ /**
+ * Passes through $cmd to the memcache server connected by $sock; returns
+ * output as an array (null array if no output)
+ *
+ * NOTE: due to a possible bug in how PHP reads while using fgets(), each
+ * line may not be terminated by a \r\n. More specifically, my testing
+ * has shown that, on FreeBSD at least, each line is terminated only
+ * with a \n. This is with the PHP flag auto_detect_line_endings set
+ * to falase (the default).
+ *
+ * @param resource $sock Socket to send command on
+ * @param string $cmd Command to run
+ *
+ * @return array Output array
+ * @access public
+ */
+ function run_command( $sock, $cmd ) {
+ if ( !is_resource( $sock ) ) {
+ return array();
+ }
+
+ if ( !$this->_safe_fwrite( $sock, $cmd, strlen( $cmd ) ) ) {
+ return array();
+ }
+
+ while ( true ) {
+ $res = fgets( $sock );
+ $ret[] = $res;
+ if ( preg_match( '/^END/', $res ) ) {
+ break;
+ }
+ if ( strlen( $res ) == 0 ) {
+ break;
+ }
+ }
+ return $ret;
+ }
+
+ // }}}
+ // {{{ set()
+
+ /**
+ * Unconditionally sets a key to a given value in the memcache. Returns true
+ * if set successfully.
+ *
+ * @param string $key Key to set value as
+ * @param mixed $value Value to set
+ * @param integer $exp (optional) Experiation time
+ *
+ * @return boolean TRUE on success
+ */
+ public function set( $key, $value, $exp = 0 ) {
+ return $this->_set( 'set', $key, $value, $exp );
+ }
+
+ // }}}
+ // {{{ set_compress_threshold()
+
+ /**
+ * Sets the compression threshold
+ *
+ * @param integer $thresh Threshold to compress if larger than
+ */
+ public function set_compress_threshold( $thresh ) {
+ $this->_compress_threshold = $thresh;
+ }
+
+ // }}}
+ // {{{ set_debug()
+
+ /**
+ * Sets the debug flag
+ *
+ * @param boolean $dbg TRUE for debugging, FALSE otherwise
+ *
+ * @see MWMemcached::__construct
+ */
+ public function set_debug( $dbg ) {
+ $this->_debug = $dbg;
+ }
+
+ // }}}
+ // {{{ set_servers()
+
+ /**
+ * Sets the server list to distribute key gets and puts between
+ *
+ * @param array $list Array of servers to connect to
+ *
+ * @see MWMemcached::__construct()
+ */
+ public function set_servers( $list ) {
+ $this->_servers = $list;
+ $this->_active = count( $list );
+ $this->_buckets = null;
+ $this->_bucketcount = 0;
+
+ $this->_single_sock = null;
+ if ( $this->_active == 1 ) {
+ $this->_single_sock = $this->_servers[0];
+ }
+ }
+
+ /**
+ * Sets the timeout for new connections
+ *
+ * @param integer $seconds Number of seconds
+ * @param integer $microseconds Number of microseconds
+ */
+ public function set_timeout( $seconds, $microseconds ) {
+ $this->_timeout_seconds = $seconds;
+ $this->_timeout_microseconds = $microseconds;
+ }
+
+ // }}}
+ // }}}
+ // {{{ private methods
+ // {{{ _close_sock()
+
+ /**
+ * Close the specified socket
+ *
+ * @param string $sock Socket to close
+ *
+ * @access private
+ */
+ function _close_sock( $sock ) {
+ $host = array_search( $sock, $this->_cache_sock );
+ fclose( $this->_cache_sock[$host] );
+ unset( $this->_cache_sock[$host] );
+ }
+
+ // }}}
+ // {{{ _connect_sock()
+
+ /**
+ * Connects $sock to $host, timing out after $timeout
+ *
+ * @param integer $sock Socket to connect
+ * @param string $host Host:IP to connect to
+ *
+ * @return boolean
+ * @access private
+ */
+ function _connect_sock( &$sock, $host ) {
+ list( $ip, $port ) = explode( ':', $host );
+ $sock = false;
+ $timeout = $this->_connect_timeout;
+ $errno = $errstr = null;
+ for( $i = 0; !$sock && $i < $this->_connect_attempts; $i++ ) {
+ if ( $this->_persistant == 1 ) {
+ $sock = @pfsockopen( $ip, $port, $errno, $errstr, $timeout );
+ } else {
+ $sock = @fsockopen( $ip, $port, $errno, $errstr, $timeout );
+ }
+ }
+ if ( !$sock ) {
+ if ( $this->_debug ) {
+ $this->_debugprint( "Error connecting to $host: $errstr\n" );
+ }
+ return false;
+ }
+
+ // Initialise timeout
+ stream_set_timeout( $sock, $this->_timeout_seconds, $this->_timeout_microseconds );
+
+ return true;
+ }
+
+ // }}}
+ // {{{ _dead_sock()
+
+ /**
+ * Marks a host as dead until 30-40 seconds in the future
+ *
+ * @param string $sock Socket to mark as dead
+ *
+ * @access private
+ */
+ function _dead_sock( $sock ) {
+ $host = array_search( $sock, $this->_cache_sock );
+ $this->_dead_host( $host );
+ }
+
+ function _dead_host( $host ) {
+ @list( $ip, /* $port */) = explode( ':', $host );
+ $this->_host_dead[$ip] = time() + 30 + intval( rand( 0, 10 ) );
+ $this->_host_dead[$host] = $this->_host_dead[$ip];
+ unset( $this->_cache_sock[$host] );
+ }
+
+ // }}}
+ // {{{ get_sock()
+
+ /**
+ * get_sock
+ *
+ * @param string $key Key to retrieve value for;
+ *
+ * @return mixed resource on success, false on failure
+ * @access private
+ */
+ function get_sock( $key ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ if ( $this->_single_sock !== null ) {
+ $this->_flush_read_buffer( $this->_single_sock );
+ return $this->sock_to_host( $this->_single_sock );
+ }
+
+ $hv = is_array( $key ) ? intval( $key[0] ) : $this->_hashfunc( $key );
+
+ if ( $this->_buckets === null ) {
+ foreach ( $this->_servers as $v ) {
+ if ( is_array( $v ) ) {
+ for( $i = 0; $i < $v[1]; $i++ ) {
+ $bu[] = $v[0];
+ }
+ } else {
+ $bu[] = $v;
+ }
+ }
+ $this->_buckets = $bu;
+ $this->_bucketcount = count( $bu );
+ }
+
+ $realkey = is_array( $key ) ? $key[1] : $key;
+ for( $tries = 0; $tries < 20; $tries++ ) {
+ $host = $this->_buckets[$hv % $this->_bucketcount];
+ $sock = $this->sock_to_host( $host );
+ if ( is_resource( $sock ) ) {
+ $this->_flush_read_buffer( $sock );
+ return $sock;
+ }
+ $hv = $this->_hashfunc( $hv . $realkey );
+ }
+
+ return false;
+ }
+
+ // }}}
+ // {{{ _hashfunc()
+
+ /**
+ * Creates a hash integer based on the $key
+ *
+ * @param string $key Key to hash
+ *
+ * @return integer Hash value
+ * @access private
+ */
+ function _hashfunc( $key ) {
+ # Hash function must on [0,0x7ffffff]
+ # We take the first 31 bits of the MD5 hash, which unlike the hash
+ # function used in a previous version of this client, works
+ return hexdec( substr( md5( $key ), 0, 8 ) ) & 0x7fffffff;
+ }
+
+ // }}}
+ // {{{ _incrdecr()
+
+ /**
+ * Perform increment/decriment on $key
+ *
+ * @param string $cmd Command to perform
+ * @param string $key Key to perform it on
+ * @param integer $amt Amount to adjust
+ *
+ * @return integer New value of $key
+ * @access private
+ */
+ function _incrdecr( $cmd, $key, $amt = 1 ) {
+ if ( !$this->_active ) {
+ return null;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return null;
+ }
+
+ $key = is_array( $key ) ? $key[1] : $key;
+ @$this->stats[$cmd]++;
+ if ( !$this->_safe_fwrite( $sock, "$cmd $key $amt\r\n" ) ) {
+ return $this->_dead_sock( $sock );
+ }
+
+ $line = fgets( $sock );
+ $match = array();
+ if ( !preg_match( '/^(\d+)/', $line, $match ) ) {
+ return null;
+ }
+ return $match[1];
+ }
+
+ // }}}
+ // {{{ _load_items()
+
+ /**
+ * Load items into $ret from $sock
+ *
+ * @param resource $sock Socket to read from
+ * @param array $ret Returned values
+ *
+ * @access private
+ */
+ function _load_items( $sock, &$ret ) {
+ while ( 1 ) {
+ $decl = fgets( $sock );
+ if ( $decl == "END\r\n" ) {
+ return true;
+ } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) {
+ list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
+ $bneed = $len + 2;
+ $offset = 0;
+
+ while ( $bneed > 0 ) {
+ $data = fread( $sock, $bneed );
+ $n = strlen( $data );
+ if ( $n == 0 ) {
+ break;
+ }
+ $offset += $n;
+ $bneed -= $n;
+ @$ret[$rkey] .= $data;
+ }
+
+ if ( $offset != $len + 2 ) {
+ // Something is borked!
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "Something is borked! key %s expecting %d got %d length\n", $rkey, $len + 2, $offset ) );
+ }
+
+ unset( $ret[$rkey] );
+ $this->_close_sock( $sock );
+ return false;
+ }
+
+ if ( $this->_have_zlib && $flags & self::COMPRESSED ) {
+ $ret[$rkey] = gzuncompress( $ret[$rkey] );
+ }
+
+ $ret[$rkey] = rtrim( $ret[$rkey] );
+
+ if ( $flags & self::SERIALIZED ) {
+ $ret[$rkey] = unserialize( $ret[$rkey] );
+ }
+
+ } else {
+ $this->_debugprint( "Error parsing memcached response\n" );
+ return 0;
+ }
+ }
+ }
+
+ // }}}
+ // {{{ _set()
+
+ /**
+ * Performs the requested storage operation to the memcache server
+ *
+ * @param string $cmd Command to perform
+ * @param string $key Key to act on
+ * @param mixed $val What we need to store
+ * @param integer $exp When it should expire
+ *
+ * @return boolean
+ * @access private
+ */
+ function _set( $cmd, $key, $val, $exp ) {
+ if ( !$this->_active ) {
+ return false;
+ }
+
+ $sock = $this->get_sock( $key );
+ if ( !is_resource( $sock ) ) {
+ return false;
+ }
+
+ @$this->stats[$cmd]++;
+
+ $flags = 0;
+
+ if ( !is_scalar( $val ) ) {
+ $val = serialize( $val );
+ $flags |= self::SERIALIZED;
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "client: serializing data as it is not scalar\n" ) );
+ }
+ }
+
+ $len = strlen( $val );
+
+ if ( $this->_have_zlib && $this->_compress_enable &&
+ $this->_compress_threshold && $len >= $this->_compress_threshold )
+ {
+ $c_val = gzcompress( $val, 9 );
+ $c_len = strlen( $c_val );
+
+ if ( $c_len < $len * ( 1 - self::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 |= self::COMPRESSED;
+ }
+ }
+ if ( !$this->_safe_fwrite( $sock, "$cmd $key $flags $exp $len\r\n$val\r\n" ) ) {
+ return $this->_dead_sock( $sock );
+ }
+
+ $line = trim( fgets( $sock ) );
+
+ if ( $this->_debug ) {
+ $this->_debugprint( sprintf( "%s %s (%s)\n", $cmd, $key, $line ) );
+ }
+ if ( $line == "STORED" ) {
+ return true;
+ }
+ return false;
+ }
+
+ // }}}
+ // {{{ sock_to_host()
+
+ /**
+ * Returns the socket for the host
+ *
+ * @param string $host Host:IP to get socket for
+ *
+ * @return mixed IO Stream or false
+ * @access private
+ */
+ function sock_to_host( $host ) {
+ if ( isset( $this->_cache_sock[$host] ) ) {
+ return $this->_cache_sock[$host];
+ }
+
+ $sock = null;
+ $now = time();
+ list( $ip, /* $port */) = explode( ':', $host );
+ if ( isset( $this->_host_dead[$host] ) && $this->_host_dead[$host] > $now ||
+ isset( $this->_host_dead[$ip] ) && $this->_host_dead[$ip] > $now
+ ) {
+ return null;
+ }
+
+ if ( !$this->_connect_sock( $sock, $host ) ) {
+ return $this->_dead_host( $host );
+ }
+
+ // Do not buffer writes
+ stream_set_write_buffer( $sock, 0 );
+
+ $this->_cache_sock[$host] = $sock;
+
+ return $this->_cache_sock[$host];
+ }
+
+ function _debugprint( $str ) {
+ print( $str );
+ }
+
+ /**
+ * Write to a stream, timing out after the correct amount of time
+ *
+ * @return bool false on failure, true on success
+ */
+ /*
+ function _safe_fwrite( $f, $buf, $len = false ) {
+ stream_set_blocking( $f, 0 );
+
+ if ( $len === false ) {
+ wfDebug( "Writing " . strlen( $buf ) . " bytes\n" );
+ $bytesWritten = fwrite( $f, $buf );
+ } else {
+ wfDebug( "Writing $len bytes\n" );
+ $bytesWritten = fwrite( $f, $buf, $len );
+ }
+ $n = stream_select( $r = null, $w = array( $f ), $e = null, 10, 0 );
+ # $this->_timeout_seconds, $this->_timeout_microseconds );
+
+ wfDebug( "stream_select returned $n\n" );
+ stream_set_blocking( $f, 1 );
+ return $n == 1;
+ return $bytesWritten;
+ }*/
+
+ /**
+ * Original behaviour
+ */
+ function _safe_fwrite( $f, $buf, $len = false ) {
+ if ( $len === false ) {
+ $bytesWritten = fwrite( $f, $buf );
+ } else {
+ $bytesWritten = fwrite( $f, $buf, $len );
+ }
+ return $bytesWritten;
+ }
+
+ /**
+ * Flush the read buffer of a stream
+ */
+ function _flush_read_buffer( $f ) {
+ if ( !is_resource( $f ) ) {
+ return;
+ }
+ $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
+ while ( $n == 1 && !feof( $f ) ) {
+ fread( $f, 1024 );
+ $n = stream_select( $r = array( $f ), $w = null, $e = null, 0, 0 );
+ }
+ }
+
+ // }}}
+ // }}}
+ // }}}
}
// vim: sts=3 sw=3 et
// }}}
+
+class MemCachedClientforWiki extends MWMemcached {
+ function _debugprint( $text ) {
+ wfDebug( "memcached: $text" );
+ }
+}
diff --git a/includes/mime.types b/includes/mime.types
index da15b5ba..bd57cd40 100644
--- a/includes/mime.types
+++ b/includes/mime.types
@@ -2,7 +2,7 @@ application/andrew-inset ez
application/mac-binhex40 hqx
application/mac-compactpro cpt
application/mathml+xml mathml
-application/msword doc
+application/msword doc docx docm dot dotx dotm
application/octet-stream bin dms lha lzh exe class so dll
application/oda oda
application/ogg ogg ogm
@@ -13,8 +13,8 @@ application/smil smi smil
application/srgs gram
application/srgs+xml grxml
application/vnd.mif mif
-application/vnd.ms-excel xls
-application/vnd.ms-powerpoint ppt
+application/vnd.ms-excel xls xlsx xlsb xlam xltx xltm
+application/vnd.ms-powerpoint ppt pptm pptx pot potx potm ppsm ppam
application/vnd.wap.wbxml wbxml
application/vnd.wap.wmlc wmlc
application/vnd.wap.wmlscriptc wmlsc
diff --git a/includes/normal/RandomTest.php b/includes/normal/RandomTest.php
index 018910cd..eb137574 100644
--- a/includes/normal/RandomTest.php
+++ b/includes/normal/RandomTest.php
@@ -32,7 +32,7 @@ if( php_sapi_name() != 'cli' ) {
/** */
require_once( 'UtfNormal.php' );
-require_once( '../DifferenceEngine.php' );
+require_once( '../diff/DifferenceEngine.php' );
dl('php_utfnormal.so' );
diff --git a/includes/normal/Utf8CaseGenerate.php b/includes/normal/Utf8CaseGenerate.php
index 8dbbb72a..22994ba4 100644
--- a/includes/normal/Utf8CaseGenerate.php
+++ b/includes/normal/Utf8CaseGenerate.php
@@ -45,7 +45,7 @@ $wikiLowerChars = array();
print "Reading character definitions...\n";
while( false !== ($line = fgets( $in ) ) ) {
- $columns = split(';', $line);
+ $columns = explode(';', $line);
$codepoint = $columns[0];
$name = $columns[1];
$simpleUpper = $columns[12];
diff --git a/includes/normal/Utf8Test.php b/includes/normal/Utf8Test.php
index 353d11b5..4c78b3db 100644
--- a/includes/normal/Utf8Test.php
+++ b/includes/normal/Utf8Test.php
@@ -81,7 +81,7 @@ $longTests = array(
# These tests are not in proper subsections
$sectionTests = array( '3.4' );
-$section = NULL;
+$section = null;
$test = '';
$failed = 0;
$success = 0;
diff --git a/includes/normal/UtfNormal.php b/includes/normal/UtfNormal.php
index 4f8b1293..e1352fdb 100644
--- a/includes/normal/UtfNormal.php
+++ b/includes/normal/UtfNormal.php
@@ -25,13 +25,13 @@
require_once dirname(__FILE__).'/UtfNormalUtil.php';
global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp;
-$utfCombiningClass = NULL;
-$utfCanonicalComp = NULL;
-$utfCanonicalDecomp = NULL;
+$utfCombiningClass = null;
+$utfCanonicalComp = null;
+$utfCanonicalDecomp = null;
# Load compatibility decompositions on demand if they are needed.
global $utfCompatibilityDecomp;
-$utfCompatibilityDecomp = NULL;
+$utfCompatibilityDecomp = null;
/**
* For using the ICU wrapper
diff --git a/includes/normal/UtfNormalData.inc b/includes/normal/UtfNormalData.inc
index cf942f68..46a93947 100644
--- a/includes/normal/UtfNormalData.inc
+++ b/includes/normal/UtfNormalData.inc
@@ -5,8 +5,8 @@
*/
/** */
global $utfCombiningClass, $utfCanonicalComp, $utfCanonicalDecomp, $utfCheckNFC;
-$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";}' );
-?>
+$utfCombiningClass = unserialize( 'a:594:{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: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: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:9;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: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:230;s:3:"᳔";i:1;s:3:"᳕";i:220;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:220;s:3:"᳝";i:220;s:3:"᳞";i:220;s:3:"᳟";i:220;s:3:"᳠";i:230;s:3:"᳢";i:1;s:3:"᳣";i:1;s:3:"᳤";i:1;s:3:"᳥";i:1;s:3:"᳦";i:1;s:3:"᳧";i:1;s:3:"᳨";i:1;s:3:"᳭";i:220;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: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: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: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:230;s:3:"꛱";i:230;s:3:"꠆";i:9;s:3:"꣄";i:9;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:220;s:3:"꤭";i:220;s:3:"꥓";i:9;s:3:"꦳";i:7;s:3:"꧀";i:9;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: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:9;s:4:"𑂺";i:7;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:1868:{s:3:"À";s:2:"À";s:3:"Á";s:2:"Á";s:3:"Â";s:2:"Â";s:3:"Ã";s:2:"Ã";s:3:"Ä";s:2:"Ä";s:3:"Å";s:2:"Å";s:3:"Ç";s:2:"Ç";s:3:"È";s:2:"È";s:3:"É";s:2:"É";s:3:"Ê";s:2:"Ê";s:3:"Ë";s:2:"Ë";s:3:"Ì";s:2:"Ì";s:3:"Í";s:2:"Í";s:3:"Î";s:2:"Î";s:3:"Ï";s:2:"Ï";s:3:"Ñ";s:2:"Ñ";s:3:"Ò";s:2:"Ò";s:3:"Ó";s:2:"Ó";s:3:"Ô";s:2:"Ô";s:3:"Õ";s:2:"Õ";s:3:"Ö";s:2:"Ö";s:3:"Ù";s:2:"Ù";s:3:"Ú";s:2:"Ú";s:3:"Û";s:2:"Û";s:3:"Ü";s:2:"Ü";s:3:"Ý";s:2:"Ý";s:3:"à";s:2:"à";s:3:"á";s:2:"á";s:3:"â";s:2:"â";s:3:"ã";s:2:"ã";s:3:"ä";s:2:"ä";s:3:"å";s:2:"å";s:3:"ç";s:2:"ç";s:3:"è";s:2:"è";s:3:"é";s:2:"é";s:3:"ê";s:2:"ê";s:3:"ë";s:2:"ë";s:3:"ì";s:2:"ì";s:3:"í";s:2:"í";s:3:"î";s:2:"î";s:3:"ï";s:2:"ï";s:3:"ñ";s:2:"ñ";s:3:"ò";s:2:"ò";s:3:"ó";s:2:"ó";s:3:"ô";s:2:"ô";s:3:"õ";s:2:"õ";s:3:"ö";s:2:"ö";s:3:"ù";s:2:"ù";s:3:"ú";s:2:"ú";s:3:"û";s:2:"û";s:3:"ü";s:2:"ü";s:3:"ý";s:2:"ý";s:3:"ÿ";s:2:"ÿ";s:3:"Ā";s:2:"Ā";s:3:"ā";s:2:"ā";s:3:"Ă";s:2:"Ă";s:3:"ă";s:2:"ă";s:3:"Ą";s:2:"Ą";s:3:"ą";s:2:"ą";s:3:"Ć";s:2:"Ć";s:3:"ć";s:2:"ć";s:3:"Ĉ";s:2:"Ĉ";s:3:"ĉ";s:2:"ĉ";s:3:"Ċ";s:2:"Ċ";s:3:"ċ";s:2:"ċ";s:3:"Č";s:2:"Č";s:3:"č";s:2:"č";s:3:"Ď";s:2:"Ď";s:3:"ď";s:2:"ď";s:3:"Ē";s:2:"Ē";s:3:"ē";s:2:"ē";s:3:"Ĕ";s:2:"Ĕ";s:3:"ĕ";s:2:"ĕ";s:3:"Ė";s:2:"Ė";s:3:"ė";s:2:"ė";s:3:"Ę";s:2:"Ę";s:3:"ę";s:2:"ę";s:3:"Ě";s:2:"Ě";s:3:"ě";s:2:"ě";s:3:"Ĝ";s:2:"Ĝ";s:3:"ĝ";s:2:"ĝ";s:3:"Ğ";s:2:"Ğ";s:3:"ğ";s:2:"ğ";s:3:"Ġ";s:2:"Ġ";s:3:"ġ";s:2:"ġ";s:3:"Ģ";s:2:"Ģ";s:3:"ģ";s:2:"ģ";s:3:"Ĥ";s:2:"Ĥ";s:3:"ĥ";s:2:"ĥ";s:3:"Ĩ";s:2:"Ĩ";s:3:"ĩ";s:2:"ĩ";s:3:"Ī";s:2:"Ī";s:3:"ī";s:2:"ī";s:3:"Ĭ";s:2:"Ĭ";s:3:"ĭ";s:2:"ĭ";s:3:"Į";s:2:"Į";s:3:"į";s:2:"į";s:3:"İ";s:2:"İ";s:3:"Ĵ";s:2:"Ĵ";s:3:"ĵ";s:2:"ĵ";s:3:"Ķ";s:2:"Ķ";s:3:"ķ";s:2:"ķ";s:3:"Ĺ";s:2:"Ĺ";s:3:"ĺ";s:2:"ĺ";s:3:"Ļ";s:2:"Ļ";s:3:"ļ";s:2:"ļ";s:3:"Ľ";s:2:"Ľ";s:3:"ľ";s:2:"ľ";s:3:"Ń";s:2:"Ń";s:3:"ń";s:2:"ń";s:3:"Ņ";s:2:"Ņ";s:3:"ņ";s:2:"ņ";s:3:"Ň";s:2:"Ň";s:3:"ň";s:2:"ň";s:3:"Ō";s:2:"Ō";s:3:"ō";s:2:"ō";s:3:"Ŏ";s:2:"Ŏ";s:3:"ŏ";s:2:"ŏ";s:3:"Ő";s:2:"Ő";s:3:"ő";s:2:"ő";s:3:"Ŕ";s:2:"Ŕ";s:3:"ŕ";s:2:"ŕ";s:3:"Ŗ";s:2:"Ŗ";s:3:"ŗ";s:2:"ŗ";s:3:"Ř";s:2:"Ř";s:3:"ř";s:2:"ř";s:3:"Ś";s:2:"Ś";s:3:"ś";s:2:"ś";s:3:"Ŝ";s:2:"Ŝ";s:3:"ŝ";s:2:"ŝ";s:3:"Ş";s:2:"Ş";s:3:"ş";s:2:"ş";s:3:"Š";s:2:"Š";s:3:"š";s:2:"š";s:3:"Ţ";s:2:"Ţ";s:3:"ţ";s:2:"ţ";s:3:"Ť";s:2:"Ť";s:3:"ť";s:2:"ť";s:3:"Ũ";s:2:"Ũ";s:3:"ũ";s:2:"ũ";s:3:"Ū";s:2:"Ū";s:3:"ū";s:2:"ū";s:3:"Ŭ";s:2:"Ŭ";s:3:"ŭ";s:2:"ŭ";s:3:"Ů";s:2:"Ů";s:3:"ů";s:2:"ů";s:3:"Ű";s:2:"Ű";s:3:"ű";s:2:"ű";s:3:"Ų";s:2:"Ų";s:3:"ų";s:2:"ų";s:3:"Ŵ";s:2:"Ŵ";s:3:"ŵ";s:2:"ŵ";s:3:"Ŷ";s:2:"Ŷ";s:3:"ŷ";s:2:"ŷ";s:3:"Ÿ";s:2:"Ÿ";s:3:"Ź";s:2:"Ź";s:3:"ź";s:2:"ź";s:3:"Ż";s:2:"Ż";s:3:"ż";s:2:"ż";s:3:"Ž";s:2:"Ž";s:3:"ž";s:2:"ž";s:3:"Ơ";s:2:"Ơ";s:3:"ơ";s:2:"ơ";s:3:"Ư";s:2:"Ư";s:3:"ư";s:2:"ư";s:3:"Ǎ";s:2:"Ǎ";s:3:"ǎ";s:2:"ǎ";s: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:4:"𤋮";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:8:"𑂚";s:4:"𑂚";s:8:"𑂜";s:4:"𑂜";s:8:"𑂫";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: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:2049:{s:2:"À";s:3:"À";s:2:"Á";s:3:"Á";s:2:"Â";s:3:"Â";s:2:"Ã";s:3:"Ã";s:2:"Ä";s:3:"Ä";s:2:"Å";s:3:"Å";s:2:"Ç";s:3:"Ç";s:2:"È";s:3:"È";s:2:"É";s:3:"É";s:2:"Ê";s:3:"Ê";s:2:"Ë";s:3:"Ë";s:2:"Ì";s:3:"Ì";s:2:"Í";s:3:"Í";s:2:"Î";s:3:"Î";s:2:"Ï";s:3:"Ï";s:2:"Ñ";s:3:"Ñ";s:2:"Ò";s:3:"Ò";s:2:"Ó";s:3:"Ó";s:2:"Ô";s:3:"Ô";s:2:"Õ";s:3:"Õ";s:2:"Ö";s:3:"Ö";s:2:"Ù";s:3:"Ù";s:2:"Ú";s:3:"Ú";s:2:"Û";s:3:"Û";s:2:"Ü";s:3:"Ü";s:2:"Ý";s:3:"Ý";s:2:"à";s:3:"à";s:2:"á";s:3:"á";s:2:"â";s:3:"â";s:2:"ã";s:3:"ã";s:2:"ä";s:3:"ä";s:2:"å";s:3:"å";s:2:"ç";s:3:"ç";s:2:"è";s:3:"è";s:2:"é";s:3:"é";s:2:"ê";s:3:"ê";s:2:"ë";s:3:"ë";s:2:"ì";s:3:"ì";s:2:"í";s:3:"í";s:2:"î";s:3:"î";s:2:"ï";s:3:"ï";s:2:"ñ";s:3:"ñ";s:2:"ò";s:3:"ò";s:2:"ó";s:3:"ó";s:2:"ô";s:3:"ô";s:2:"õ";s:3:"õ";s:2:"ö";s:3:"ö";s:2:"ù";s:3:"ù";s:2:"ú";s:3:"ú";s:2:"û";s:3:"û";s:2:"ü";s:3:"ü";s:2:"ý";s:3:"ý";s:2:"ÿ";s:3:"ÿ";s:2:"Ā";s:3:"Ā";s:2:"ā";s:3:"ā";s:2:"Ă";s:3:"Ă";s:2:"ă";s:3:"ă";s:2:"Ą";s:3:"Ą";s:2:"ą";s:3:"ą";s:2:"Ć";s:3:"Ć";s:2:"ć";s:3:"ć";s:2:"Ĉ";s:3:"Ĉ";s:2:"ĉ";s:3:"ĉ";s:2:"Ċ";s:3:"Ċ";s:2:"ċ";s:3:"ċ";s:2:"Č";s:3:"Č";s:2:"č";s:3:"č";s:2:"Ď";s:3:"Ď";s:2:"ď";s:3:"ď";s:2:"Ē";s:3:"Ē";s:2:"ē";s:3:"ē";s:2:"Ĕ";s:3:"Ĕ";s:2:"ĕ";s:3:"ĕ";s:2:"Ė";s:3:"Ė";s:2:"ė";s:3:"ė";s:2:"Ę";s:3:"Ę";s:2:"ę";s:3:"ę";s:2:"Ě";s:3:"Ě";s:2:"ě";s:3:"ě";s:2:"Ĝ";s:3:"Ĝ";s:2:"ĝ";s:3:"ĝ";s:2:"Ğ";s:3:"Ğ";s:2:"ğ";s:3:"ğ";s:2:"Ġ";s:3:"Ġ";s:2:"ġ";s:3:"ġ";s:2:"Ģ";s:3:"Ģ";s:2:"ģ";s:3:"ģ";s:2:"Ĥ";s:3:"Ĥ";s:2:"ĥ";s:3:"ĥ";s:2:"Ĩ";s:3:"Ĩ";s:2:"ĩ";s:3:"ĩ";s:2:"Ī";s:3:"Ī";s:2:"ī";s:3:"ī";s:2:"Ĭ";s:3:"Ĭ";s:2:"ĭ";s:3:"ĭ";s:2:"Į";s:3:"Į";s:2:"į";s:3:"į";s:2:"İ";s:3:"İ";s:2:"Ĵ";s:3:"Ĵ";s:2:"ĵ";s:3:"ĵ";s:2:"Ķ";s:3:"Ķ";s:2:"ķ";s:3:"ķ";s:2:"Ĺ";s:3:"Ĺ";s:2:"ĺ";s:3:"ĺ";s:2:"Ļ";s:3:"Ļ";s:2:"ļ";s:3:"ļ";s:2:"Ľ";s:3:"Ľ";s:2:"ľ";s:3:"ľ";s:2:"Ń";s:3:"Ń";s:2:"ń";s:3:"ń";s:2:"Ņ";s:3:"Ņ";s:2:"ņ";s:3:"ņ";s:2:"Ň";s:3:"Ň";s:2:"ň";s:3:"ň";s:2:"Ō";s:3:"Ō";s:2:"ō";s:3:"ō";s:2:"Ŏ";s:3:"Ŏ";s:2:"ŏ";s:3:"ŏ";s:2:"Ő";s:3:"Ő";s:2:"ő";s:3:"ő";s:2:"Ŕ";s:3:"Ŕ";s:2:"ŕ";s:3:"ŕ";s:2:"Ŗ";s:3:"Ŗ";s:2:"ŗ";s:3:"ŗ";s:2:"Ř";s:3:"Ř";s:2:"ř";s:3:"ř";s:2:"Ś";s:3:"Ś";s:2:"ś";s:3:"ś";s:2:"Ŝ";s:3:"Ŝ";s:2:"ŝ";s:3:"ŝ";s:2:"Ş";s:3:"Ş";s:2:"ş";s:3:"ş";s:2:"Š";s:3:"Š";s:2:"š";s:3:"š";s:2:"Ţ";s:3:"Ţ";s:2:"ţ";s:3:"ţ";s:2:"Ť";s:3:"Ť";s:2:"ť";s:3:"ť";s:2:"Ũ";s:3:"Ũ";s:2:"ũ";s:3:"ũ";s:2:"Ū";s:3:"Ū";s:2:"ū";s:3:"ū";s:2:"Ŭ";s:3:"Ŭ";s:2:"ŭ";s:3:"ŭ";s:2:"Ů";s:3:"Ů";s:2:"ů";s:3:"ů";s:2:"Ű";s:3:"Ű";s:2:"ű";s:3:"ű";s:2:"Ų";s:3:"Ų";s:2:"ų";s:3:"ų";s:2:"Ŵ";s:3:"Ŵ";s:2:"ŵ";s:3:"ŵ";s:2:"Ŷ";s:3:"Ŷ";s:2:"ŷ";s:3:"ŷ";s:2:"Ÿ";s:3:"Ÿ";s:2:"Ź";s:3:"Ź";s:2:"ź";s:3:"ź";s:2:"Ż";s:3:"Ż";s:2:"ż";s:3:"ż";s:2:"Ž";s:3:"Ž";s:2:"ž";s:3:"ž";s:2:"Ơ";s:3:"Ơ";s:2:"ơ";s:3:"ơ";s:2:"Ư";s:3:"Ư";s:2:"ư";s:3:"ư";s:2:"Ǎ";s:3:"Ǎ";s:2:"ǎ";s:3:"ǎ";s:2:"Ǐ";s: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: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:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s: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:8:"𑂫";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:1221:{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: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";s:4:"𑂺";s:1:"M";}' );
+
diff --git a/includes/normal/UtfNormalDataK.inc b/includes/normal/UtfNormalDataK.inc
index a97e005a..ad1b4bf2 100644
--- a/includes/normal/UtfNormalDataK.inc
+++ b/includes/normal/UtfNormalDataK.inc
@@ -5,5 +5,5 @@
*/
/** */
global $utfCompatibilityDecomp;
-$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:"𪘀";}' );
-?>
+$utfCompatibilityDecomp = unserialize( 'a:5516:{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⁄7";s:3:"⅑";s:5:"1⁄9";s:3:"⅒";s:6:"1⁄10";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:"0⁄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: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:"問";s:3:"㉅";s:3:"幼";s:3:"㉆";s:3:"文";s:3:"㉇";s:3:"箏";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: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:3:"贈";s:3:"輸";s:3:"輸";s:3:"遲";s:3:"遲";s:3:"醙";s:3:"醙";s:3:"鉶";s:3:"鉶";s:3:"陼";s:3:"陼";s:3:"難";s:3:"難";s:3:"靖";s:3:"靖";s:3:"韛";s: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:8:"𑂫";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:2:"0.";s:4:"🄁";s:2:"0,";s:4:"🄂";s:2:"1,";s:4:"🄃";s:2:"2,";s:4:"🄄";s:2:"3,";s:4:"🄅";s:2:"4,";s:4:"🄆";s:2:"5,";s:4:"🄇";s:2:"6,";s:4:"🄈";s:2:"7,";s:4:"🄉";s:2:"8,";s:4:"🄊";s:2:"9,";s:4:"🄐";s:3:"(A)";s:4:"🄑";s:3:"(B)";s:4:"🄒";s:3:"(C)";s:4:"🄓";s:3:"(D)";s:4:"🄔";s:3:"(E)";s:4:"🄕";s:3:"(F)";s:4:"🄖";s:3:"(G)";s:4:"🄗";s:3:"(H)";s:4:"🄘";s:3:"(I)";s:4:"🄙";s:3:"(J)";s:4:"🄚";s:3:"(K)";s:4:"🄛";s:3:"(L)";s:4:"🄜";s:3:"(M)";s:4:"🄝";s:3:"(N)";s:4:"🄞";s:3:"(O)";s:4:"🄟";s:3:"(P)";s:4:"🄠";s:3:"(Q)";s:4:"🄡";s:3:"(R)";s:4:"🄢";s:3:"(S)";s:4:"🄣";s:3:"(T)";s:4:"🄤";s:3:"(U)";s:4:"🄥";s:3:"(V)";s:4:"🄦";s:3:"(W)";s:4:"🄧";s:3:"(X)";s:4:"🄨";s:3:"(Y)";s:4:"🄩";s:3:"(Z)";s:4:"🄪";s:7:"〔S〕";s:4:"🄫";s:1:"C";s:4:"🄬";s:1:"R";s:4:"🄭";s:2:"CD";s:4:"🄮";s:2:"WZ";s:4:"🄱";s:1:"B";s:4:"🄽";s:1:"N";s:4:"🄿";s:1:"P";s:4:"🅂";s:1:"S";s:4:"🅆";s:1:"W";s:4:"🅊";s:2:"HV";s:4:"🅋";s:2:"MV";s:4:"🅌";s:2:"SD";s:4:"🅍";s:2:"SS";s:4:"🅎";s:3:"PPV";s:4:"🆐";s:2:"DJ";s:4:"🈀";s:6:"ほか";s:4:"🈐";s:3:"手";s:4:"🈑";s:3:"字";s:4:"🈒";s:3:"双";s:4:"🈓";s:6:"デ";s:4:"🈔";s:3:"二";s:4:"🈕";s:3:"多";s:4:"🈖";s:3:"解";s:4:"🈗";s:3:"天";s:4:"🈘";s:3:"交";s:4:"🈙";s:3:"映";s:4:"🈚";s:3:"無";s:4:"🈛";s:3:"料";s:4:"🈜";s:3:"前";s:4:"🈝";s:3:"後";s:4:"🈞";s:3:"再";s:4:"🈟";s:3:"新";s:4:"🈠";s:3:"初";s:4:"🈡";s:3:"終";s:4:"🈢";s:3:"生";s:4:"🈣";s:3:"販";s:4:"🈤";s:3:"声";s:4:"🈥";s:3:"吹";s:4:"🈦";s:3:"演";s:4:"🈧";s:3:"投";s:4:"🈨";s:3:"捕";s:4:"🈩";s:3:"一";s:4:"🈪";s:3:"三";s:4:"🈫";s:3:"遊";s:4:"🈬";s:3:"左";s:4:"🈭";s:3:"中";s:4:"🈮";s:3:"右";s:4:"🈯";s:3:"指";s:4:"🈰";s:3:"走";s:4:"🈱";s:3:"打";s:4:"🉀";s:9:"〔本〕";s:4:"🉁";s:9:"〔三〕";s:4:"🉂";s:9:"〔二〕";s:4:"🉃";s:9:"〔安〕";s:4:"🉄";s:9:"〔点〕";s:4:"🉅";s:9:"〔打〕";s:4:"🉆";s:9:"〔盗〕";s:4:"🉇";s:9:"〔勝〕";s:4:"🉈";s: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/UtfNormalGenerate.php b/includes/normal/UtfNormalGenerate.php
index a16e76a8..3b1e9e73 100644
--- a/includes/normal/UtfNormalGenerate.php
+++ b/includes/normal/UtfNormalGenerate.php
@@ -37,7 +37,7 @@ $in = fopen("DerivedNormalizationProps.txt", "rt" );
if( !$in ) {
print "Can't open DerivedNormalizationProps.txt for reading.\n";
print "If necessary, fetch this file from the internet:\n";
- print "http://www.unicode.org/Public/UNIDATA/CompositionExclusions.txt\n";
+ print "http://www.unicode.org/Public/UNIDATA/DerivedNormalizationProps.txt\n";
exit(-1);
}
print "Initializing normalization quick check tables...\n";
@@ -91,7 +91,7 @@ $canon = 0;
print "Reading character definitions...\n";
while( false !== ($line = fgets( $in ) ) ) {
- $columns = split(';', $line);
+ $columns = explode(';', $line);
$codepoint = $columns[0];
$name = $columns[1];
$canonicalCombiningClass = $columns[3];
@@ -182,7 +182,7 @@ global \$utfCombiningClass, \$utfCanonicalComp, \$utfCanonicalDecomp, \$utfCheck
\$utfCanonicalComp = unserialize( '$serComp' );
\$utfCanonicalDecomp = unserialize( '$serCanon' );
\$utfCheckNFC = unserialize( '$serCheckNFC' );
-?" . ">\n";
+\n";
fputs( $out, $outdata );
fclose( $out );
print "Wrote out UtfNormalData.inc\n";
@@ -203,7 +203,7 @@ if( $out ) {
/** */
global \$utfCompatibilityDecomp;
\$utfCompatibilityDecomp = unserialize( '$serCompat' );
-?" . ">\n";
+\n";
fputs( $out, $outdata );
fclose( $out );
print "Wrote out UtfNormalDataK.inc\n";
diff --git a/includes/parser/CoreParserFunctions.php b/includes/parser/CoreParserFunctions.php
index 774e96a7..8abcc04f 100644
--- a/includes/parser/CoreParserFunctions.php
+++ b/includes/parser/CoreParserFunctions.php
@@ -16,6 +16,7 @@ class CoreParserFunctions {
$parser->setFunctionHook( 'int', array( __CLASS__, 'intFunction' ), SFH_NO_HASH );
$parser->setFunctionHook( 'ns', array( __CLASS__, 'ns' ), SFH_NO_HASH );
+ $parser->setFunctionHook( 'nse', array( __CLASS__, 'nse' ), 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 );
@@ -67,7 +68,7 @@ class CoreParserFunctions {
$parser->setFunctionHook( 'subjectpagename', array( __CLASS__, 'subjectpagename' ), SFH_NO_HASH );
$parser->setFunctionHook( 'subjectpagenamee', array( __CLASS__, 'subjectpagenamee' ), SFH_NO_HASH );
$parser->setFunctionHook( 'tag', array( __CLASS__, 'tagObj' ), SFH_OBJECT_ARGS );
- $parser->setFunctionHook( 'formatdate', array( __CLASS__, 'formatDate' ) );
+ $parser->setFunctionHook( 'formatdate', array( __CLASS__, 'formatDate' ) );
if ( $wgAllowDisplayTitle ) {
$parser->setFunctionHook( 'displaytitle', array( __CLASS__, 'displaytitle' ), SFH_NO_HASH );
@@ -88,20 +89,20 @@ class CoreParserFunctions {
return array( 'found' => false );
}
}
-
+
static function formatDate( $parser, $date, $defaultPref = null ) {
$df = DateFormatter::getInstance();
-
- $date = trim($date);
-
+
+ $date = trim( $date );
+
$pref = $parser->mOptions->getDateFormat();
-
+
// Specify a different default date format other than the the normal default
- // iff the user has 'default' for their setting
- if ($pref == 'default' && $defaultPref)
+ // iff the user has 'default' for their setting
+ if ( $pref == 'default' && $defaultPref )
$pref = $defaultPref;
-
- $date = $df->reformat( $pref, $date, array('match-whole') );
+
+ $date = $df->reformat( $pref, $date, array( 'match-whole' ) );
return $date;
}
@@ -119,6 +120,10 @@ class CoreParserFunctions {
}
}
+ static function nse( $parser, $part1 = '' ) {
+ return wfUrlencode( str_replace( ' ', '_', self::ns( $parser, $part1 ) ) );
+ }
+
static function urlencode( $parser, $s = '' ) {
return urlencode( $s );
}
@@ -163,11 +168,11 @@ class CoreParserFunctions {
# 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 ) );
+ $title = Title::newFromURL( urldecode( $s ) );
if( !is_null( $title ) ) {
# Convert NS_MEDIA -> NS_FILE
if( $title->getNamespace() == NS_MEDIA ) {
- $title = Title::makeTitle( NS_FILE, $title->getDBKey() );
+ $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
}
if( !is_null( $arg ) ) {
$text = $title->$func( $arg );
@@ -193,15 +198,16 @@ class CoreParserFunctions {
}
static function gender( $parser, $user ) {
+ wfProfileIn( __METHOD__ );
$forms = array_slice( func_get_args(), 2);
// default
$gender = User::getDefaultOption( 'gender' );
-
+
// allow prefix.
$title = Title::newFromText( $user );
-
- if (is_object( $title ) && $title->getNamespace() == NS_USER)
+
+ if ( is_object( $title ) && $title->getNamespace() == NS_USER )
$user = $title->getText();
// check parameter, or use $wgUser if in interface message
@@ -212,10 +218,12 @@ class CoreParserFunctions {
global $wgUser;
$gender = $wgUser->getOption( 'gender' );
}
- return $parser->getFunctionLang()->gender( $gender, $forms );
+ $ret = $parser->getFunctionLang()->gender( $gender, $forms );
+ wfProfileOut( __METHOD__ );
+ return $ret;
}
- static function plural( $parser, $text = '') {
- $forms = array_slice( func_get_args(), 2);
+ static function plural( $parser, $text = '' ) {
+ $forms = array_slice( func_get_args(), 2 );
$text = $parser->getFunctionLang()->parseFormattedNumber( $text );
return $parser->getFunctionLang()->convertPlural( $text, $forms );
}
@@ -224,21 +232,39 @@ class CoreParserFunctions {
* 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
+ * @param $parser Parser: parent parser
+ * @param $text String: desired title text
+ * @return String
*/
static function displaytitle( $parser, $text = '' ) {
global $wgRestrictDisplayTitle;
- $text = trim( Sanitizer::decodeCharReferences( $text ) );
- if ( !$wgRestrictDisplayTitle ) {
+ #parse a limited subset of wiki markup (just the single quote items)
+ $text = $parser->doQuotes( $text );
+
+ #remove stripped text (e.g. the UNIQ-QINU stuff) that was generated by tag extensions/whatever
+ $text = preg_replace( '/' . preg_quote( $parser->uniqPrefix(), '/' ) . '.*?'
+ . preg_quote( Parser::MARKER_SUFFIX, '/' ) . '/', '', $text );
+
+ #list of disallowed tags for DISPLAYTITLE
+ #these will be escaped even though they are allowed in normal wiki text
+ $bad = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'blockquote', 'ol', 'ul', 'li', 'hr',
+ 'table', 'tr', 'th', 'td', 'dl', 'dd', 'caption', 'p', 'ruby', 'rb', 'rt', 'rp', 'br' );
+
+ #only requested titles that normalize to the actual title are allowed through
+ #if $wgRestrictDisplayTitle is true (it is by default)
+ #mimic the escaping process that occurs in OutputPage::setPageTitle
+ $text = Sanitizer::normalizeCharReferences( Sanitizer::removeHTMLtags( $text, null, array(), array(), $bad ) );
+ $title = Title::newFromText( Sanitizer::stripAllTags( $text ) );
+
+ if( !$wgRestrictDisplayTitle ) {
$parser->mOutput->setDisplayTitle( $text );
} else {
- $title = Title::newFromText( $text );
- if( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) )
+ if ( $title instanceof Title && $title->getFragment() == '' && $title->equals( $parser->mTitle ) ) {
$parser->mOutput->setDisplayTitle( $text );
+ }
}
+
return '';
}
@@ -291,9 +317,9 @@ class CoreParserFunctions {
}
static function numberingroup( $parser, $name = '', $raw = null) {
return self::formatRaw( SiteStats::numberingroup( strtolower( $name ) ), $raw );
- }
+ }
+
-
/**
* Given a title, return the namespace name that would be given by the
* corresponding magic word
@@ -302,37 +328,37 @@ class CoreParserFunctions {
*/
static function mwnamespace( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return str_replace( '_', ' ', $t->getNsText() );
}
static function namespacee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return wfUrlencode( $t->getNsText() );
}
static function talkspace( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) || !$t->canTalk() )
+ if ( is_null( $t ) || !$t->canTalk() )
return '';
return str_replace( '_', ' ', $t->getTalkNsText() );
}
static function talkspacee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) || !$t->canTalk() )
+ if ( is_null( $t ) || !$t->canTalk() )
return '';
return wfUrlencode( $t->getTalkNsText() );
}
static function subjectspace( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return str_replace( '_', ' ', $t->getSubjectNsText() );
}
static function subjectspacee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return wfUrlencode( $t->getSubjectNsText() );
}
@@ -342,77 +368,77 @@ class CoreParserFunctions {
*/
static function pagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return wfEscapeWikiText( $t->getText() );
}
static function pagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return $t->getPartialURL();
}
static function fullpagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) || !$t->canTalk() )
+ if ( is_null( $t ) || !$t->canTalk() )
return '';
return wfEscapeWikiText( $t->getPrefixedText() );
}
static function fullpagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) || !$t->canTalk() )
+ if ( is_null( $t ) || !$t->canTalk() )
return '';
return $t->getPrefixedURL();
}
static function subpagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return $t->getSubpageText();
}
static function subpagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return $t->getSubpageUrlForm();
}
static function basepagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return $t->getBaseText();
}
static function basepagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return wfUrlEncode( str_replace( ' ', '_', $t->getBaseText() ) );
- }
+ }
static function talkpagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) || !$t->canTalk() )
+ if ( is_null( $t ) || !$t->canTalk() )
return '';
return wfEscapeWikiText( $t->getTalkPage()->getPrefixedText() );
}
static function talkpagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) || !$t->canTalk() )
+ if ( is_null( $t ) || !$t->canTalk() )
return '';
return $t->getTalkPage()->getPrefixedUrl();
}
static function subjectpagename( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return wfEscapeWikiText( $t->getSubjectPage()->getPrefixedText() );
}
static function subjectpagenamee( $parser, $title = null ) {
$t = Title::newFromText( $title );
- if ( is_null($t) )
+ if ( is_null( $t ) )
return '';
return $t->getSubjectPage()->getPrefixedUrl();
}
-
+
/**
* 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
@@ -443,16 +469,16 @@ class CoreParserFunctions {
* 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
+ * @todo Fixme: This doesn't work correctly on preview for getting the size
+ * of the current page.
+ * @todo 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);
+ $title = Title::newFromText( $page );
if( !is_object( $title ) ) {
$cache[$page] = 0;
@@ -466,16 +492,16 @@ class CoreParserFunctions {
if( isset( $cache[$page] ) ) {
$length = $cache[$page];
} elseif( $parser->incrementExpensiveFunctionCount() ) {
- $rev = Revision::newFromTitle($title);
+ $rev = Revision::newFromTitle( $title );
$id = $rev ? $rev->getPage() : 0;
$length = $cache[$page] = $rev ? $rev->getSize() : 0;
-
+
// Register dependency in templatelinks
$parser->mOutput->addTemplate( $title, $id, $rev ? $rev->getId() : 0 );
- }
+ }
return self::formatRaw( $length, $raw );
}
-
+
/**
* Returns the requested protection level for the current page
*/
@@ -496,12 +522,12 @@ class CoreParserFunctions {
* Unicode-safe str_pad with the restriction that $length is forced to be <= 500
*/
static function pad( $string, $length, $padding = '0', $direction = STR_PAD_RIGHT ) {
- $lengthOfPadding = mb_strlen( $padding );
+ $lengthOfPadding = mb_strlen( $padding );
if ( $lengthOfPadding == 0 ) return $string;
-
+
# The remaining length to add counts down to 0 as padding is added
$length = min( $length, 500 ) - mb_strlen( $string );
- # $finalPadding is just $padding repeated enough times so that
+ # $finalPadding is just $padding repeated enough times so that
# mb_strlen( $string ) + mb_strlen( $finalPadding ) == $length
$finalPadding = '';
while ( $length > 0 ) {
@@ -510,7 +536,7 @@ class CoreParserFunctions {
$finalPadding .= mb_substr( $padding, 0, $length );
$length -= $lengthOfPadding;
}
-
+
if ( $direction == STR_PAD_LEFT ) {
return $finalPadding . $string;
} else {
diff --git a/includes/parser/CoreTagHooks.php b/includes/parser/CoreTagHooks.php
new file mode 100644
index 00000000..7cc8260e
--- /dev/null
+++ b/includes/parser/CoreTagHooks.php
@@ -0,0 +1,49 @@
+<?php
+
+class CoreTagHooks {
+ static function register( $parser ) {
+ global $wgRawHtml, $wgUseTeX;
+ $parser->setHook( 'pre', array( __CLASS__, 'pre' ) );
+ $parser->setHook( 'nowiki', array( __CLASS__, 'nowiki' ) );
+ $parser->setHook( 'gallery', array( __CLASS__, 'gallery' ) );
+ if ( $wgRawHtml ) {
+ $parser->setHook( 'html', array( __CLASS__, 'html' ) );
+ }
+ if ( $wgUseTeX ) {
+ $parser->setHook( 'math', array( __CLASS__, 'math' ) );
+ }
+ }
+
+ static function pre( $text, $attribs, $parser ) {
+ // Backwards-compatibility hack
+ $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
+
+ $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
+ return Xml::openElement( 'pre', $attribs ) .
+ Xml::escapeTagsOnly( $content ) .
+ '</pre>';
+ }
+
+ static function html( $content, $attributes, $parser ) {
+ global $wgRawHtml;
+ if( $wgRawHtml ) {
+ return array( $content, 'markerType' => 'nowiki' );
+ } else {
+ throw new MWException( '<html> extension tag encountered unexpectedly' );
+ }
+ }
+
+ static function nowiki( $content, $attributes, $parser ) {
+ $content = strtr( $content, array( '-{' => '-&#123;', '}-' => '&#125;-' ) );
+ return array( Xml::escapeTagsOnly( $content ), 'markerType' => 'nowiki' );
+ }
+
+ static function math( $content, $attributes, $parser ) {
+ global $wgContLang;
+ return $wgContLang->armourMath( MathRenderer::renderMath( $content, $attributes ) );
+ }
+
+ static function gallery( $content, $attributes, $parser ) {
+ return $parser->renderImageGallery( $content, $attributes );
+ }
+}
diff --git a/includes/parser/DateFormatter.php b/includes/parser/DateFormatter.php
index aa6415e4..602bcff3 100644
--- a/includes/parser/DateFormatter.php
+++ b/includes/parser/DateFormatter.php
@@ -48,10 +48,10 @@ class DateFormatter
$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::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}";
@@ -268,7 +268,7 @@ class DateFormatter
$isoDate = implode( '-', $isoBits );;
// Output is not strictly HTML (it's wikitext), but <span> is whitelisted.
- $text = Xml::tags( 'span',
+ $text = Html::rawElement( 'span',
array( 'class' => 'mw-formatted-date', 'title' => $isoDate ), $text );
return $text;
diff --git a/includes/parser/LinkHolderArray.php b/includes/parser/LinkHolderArray.php
index 35b672b9..4f382a4f 100644
--- a/includes/parser/LinkHolderArray.php
+++ b/includes/parser/LinkHolderArray.php
@@ -105,6 +105,7 @@ class LinkHolderArray {
}
/**
+ * FIXME: update documentation. makeLinkObj() is deprecated.
* 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.
@@ -228,10 +229,12 @@ class LinkHolderArray {
$linkCache->addBadLinkObj( $title );
$colours[$pdbk] = 'new';
$output->addLink( $title, 0 );
+ // FIXME: replace deprecated makeBrokenLinkObj() by link()
$replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
$entry['text'],
$query );
} else {
+ // FIXME: replace deprecated makeColouredLinkObj() by link()
$replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
$entry['text'],
$query );
diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php
index e6a68782..4f672f5b 100644
--- a/includes/parser/Parser.php
+++ b/includes/parser/Parser.php
@@ -91,8 +91,9 @@ class Parser
*/
# Persistent:
var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
- $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
- $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
+ $mSubstWords, $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex,
+ $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList,
+ $mVarCache, $mConf, $mFunctionTagHooks;
# Cleared with clearState():
@@ -103,7 +104,6 @@ class Parser
var $mTplExpandCache; // empty-frame expansion cache
var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
var $mExpensiveFunctionCount; // number of expensive parser function calls
- var $mFileCache;
# Temporary
# These are variables reset at least once per parse regardless of $clearState
@@ -127,8 +127,9 @@ class Parser
$this->mTagHooks = array();
$this->mTransparentTagHooks = array();
$this->mFunctionHooks = array();
+ $this->mFunctionTagHooks = array();
$this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
- $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+ $this->mDefaultStripList = $this->mStripList = array();
$this->mUrlProtocols = wfUrlProtocols();
$this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
'[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
@@ -171,8 +172,8 @@ class Parser
wfProfileIn( __METHOD__ );
- $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
CoreParserFunctions::register( $this );
+ CoreTagHooks::register( $this );
$this->initialiseVariables();
wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
@@ -200,6 +201,7 @@ class Parser
$this->mLinkHolders = new LinkHolderArray( $this );
$this->mLinkID = 0;
$this->mRevisionTimestamp = $this->mRevisionId = null;
+ $this->mVarCache = array();
/**
* Prefix for temporary replacement strings for the multipass parser.
@@ -230,7 +232,6 @@ class Parser
$this->mHeadings = array();
$this->mDoubleUnderscores = array();
$this->mExpensiveFunctionCount = 0;
- $this->mFileCache = array();
# Fix cloning
if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
@@ -255,9 +256,10 @@ class Parser
* Set the context title
*/
function setTitle( $t ) {
- if ( !$t || $t instanceof FakeTitle ) {
- $t = Title::newFromText( 'NO TITLE' );
- }
+ 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;
@@ -274,7 +276,7 @@ class Parser
*/
function uniqPrefix() {
if( !isset( $this->mUniqPrefix ) ) {
- // @fixme this is probably *horribly wrong*
+ // @todo 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.
@@ -303,7 +305,7 @@ class Parser
* to internalParse() which does all the real work.
*/
- global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
+ global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang, $wgDisableLangConversion, $wgDisableTitleConversion;
$fname = __METHOD__.'-' . wfGetCaller();
wfProfileIn( __METHOD__ );
wfProfileIn( $fname );
@@ -313,7 +315,8 @@ class Parser
}
$this->mOptions = $options;
- $this->setTitle( $title );
+ $this->setTitle( $title ); // Page title has to be set for the pre-processor
+
$oldRevisionId = $this->mRevisionId;
$oldRevisionTimestamp = $this->mRevisionTimestamp;
if( $revid !== null ) {
@@ -325,6 +328,7 @@ class Parser
# 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
@@ -342,11 +346,51 @@ class Parser
$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 );
+ /**
+ * The page doesn't get language converted if
+ * a) It's disabled
+ * b) Content isn't converted
+ * c) It's a conversion table
+ */
+ if ( !( $wgDisableLangConversion
+ || isset( $this->mDoubleUnderscores['nocontentconvert'] )
+ || $this->mTitle->isConversionTable() ) ) {
+
+ # The position of the convert() call should not be changed. it
+ # assumes that the links are all replaced and the only thing left
+ # is the <nowiki> mark.
+
+ $text = $wgContLang->convert( $text );
+ }
+
+ /**
+ * A page get its title converted except:
+ * a) Language conversion is globally disabled
+ * b) Title convert is globally disabled
+ * c) The page is a redirect page
+ * d) User request with a "linkconvert" set to "no"
+ * e) A "nocontentconvert" magic word has been set
+ * f) A "notitleconvert" magic word has been set
+ * g) User sets "noconvertlink" in his/her preference
+ *
+ * Note that if a user tries to set a title in a conversion
+ * rule but content conversion was not done, then the parser
+ * won't pick it up. This is probably expected behavior.
+ */
+ if ( !( $wgDisableLangConversion
+ || $wgDisableTitleConversion
+ || isset( $this->mDoubleUnderscores['nocontentconvert'] )
+ || isset( $this->mDoubleUnderscores['notitleconvert'] )
+ || $this->mOutput->getDisplayTitle() !== false ) )
+ {
+ $convruletitle = $wgContLang->getConvRuleTitle();
+ if ( $convruletitle ) {
+ $this->mOutput->setTitleText( $convruletitle );
+ } else {
+ $titleText = $wgContLang->convertTitle( $title );
+ $this->mOutput->setTitleText( $titleText );
+ }
+ }
$text = $this->mStripState->unstripNoWiki( $text );
@@ -412,7 +456,6 @@ class Parser
# 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 =
@@ -425,6 +468,7 @@ class Parser
$text .= "\n<!-- \n$limitReport-->\n";
}
$this->mOutput->setText( $text );
+
$this->mRevisionId = $oldRevisionId;
$this->mRevisionTimestamp = $oldRevisionTimestamp;
wfProfileOut( $fname );
@@ -436,12 +480,17 @@ class Parser
/**
* Recursive parser entry point that can be called from an extension tag
* hook.
+ *
+ * If $frame is not provided, then template variables (e.g., {{{1}}}) within $text are not expanded
+ *
+ * @param $text String: text extension wants to have parsed
+ * @param PPFrame $frame: The frame to use for expanding any template variables
*/
- function recursiveTagParse( $text ) {
+ function recursiveTagParse( $text, $frame=false ) {
wfProfileIn( __METHOD__ );
wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
- $text = $this->internalParse( $text );
+ $text = $this->internalParse( $text, false, $frame );
wfProfileOut( __METHOD__ );
return $text;
}
@@ -529,9 +578,9 @@ class Parser
$matches = array();
$taglist = implode( '|', $elements );
- $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
+ $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
- while ( '' != $text ) {
+ while ( $text != '' ) {
$p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
$stripped .= $p[0];
if( count( $p ) < 5 ) {
@@ -589,15 +638,7 @@ class Parser
* 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;
+ return $this->mStripList;
}
/**
@@ -648,14 +689,14 @@ class Parser
$this->mStripState->general->setPair( $rnd, $text );
return $rnd;
}
-
+
/**
* Interface with html tidy
* @deprecated Use MWTidy::tidy()
*/
public static function tidy( $text ) {
wfDeprecated( __METHOD__ );
- return MWTidy::tidy( $text );
+ return MWTidy::tidy( $text );
}
/**
@@ -693,11 +734,11 @@ class Parser
$attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
$outLine = 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 );
+ 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
$out .= $outLine."\n";
@@ -726,9 +767,9 @@ class Parser
// 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 );
+ $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
+ array_pop( $tr_attributes );
+ array_push( $tr_attributes, $attributes );
$line = '';
$last_tag = array_pop ( $last_tag_history );
@@ -862,17 +903,33 @@ class Parser
*
* @private
*/
- function internalParse( $text ) {
- $isMain = true;
+ function internalParse( $text, $isMain = true, $frame=false ) {
wfProfileIn( __METHOD__ );
+ $origText = $text;
+
# Hook to suspend the parser in this state
if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
wfProfileOut( __METHOD__ );
return $text ;
}
- $text = $this->replaceVariables( $text );
+ // if $frame is provided, then use $frame for replacing any variables
+ if ($frame) {
+ // use frame depth to infer how include/noinclude tags should be handled
+ // depth=0 means this is the top-level document; otherwise it's an included document
+ if( !$frame->depth )
+ $flag = 0;
+ else
+ $flag = Parser::PTD_FOR_INCLUSION;
+ $dom = $this->preprocessToDom( $text, $flag );
+ $text = $frame->expand( $dom );
+ }
+ // if $frame is not provided, then use old-style replaceVariables
+ else {
+ $text = $this->replaceVariables( $text );
+ }
+
$text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
@@ -885,6 +942,7 @@ class Parser
$text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
$text = $this->doDoubleUnderscore( $text );
+
$text = $this->doHeadings( $text );
if( $this->mOptions->getUseDynamicDates() ) {
$df = DateFormatter::getInstance();
@@ -899,7 +957,7 @@ class Parser
$text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
$text = $this->doMagicLinks( $text );
- $text = $this->formatHeadings( $text, $isMain );
+ $text = $this->formatHeadings( $text, $origText, $isMain );
wfProfileOut( __METHOD__ );
return $text;
@@ -908,7 +966,7 @@ class Parser
/**
* Replace special strings like "ISBN xxx" and "RFC xxx" with
* magic external links.
- *
+ *
* DML
* @private
*/
@@ -918,7 +976,7 @@ class Parser
$urlChar = self::EXT_LINK_URL_CLASS;
$text = preg_replace_callback(
'!(?: # Start cases
- (<a.*?</a>) | # m[1]: Skip link text
+ (<a.*?</a>) | # m[1]: Skip link text
(<.*?>) | # m[2]: Skip stuff inside HTML elements' . "
(\\b(?:$prots)$urlChar+) | # m[3]: Free external links" . '
(?:RFC|PMID)\s+([0-9]+) | # m[4]: RFC or PMID, capture number
@@ -944,13 +1002,16 @@ class Parser
return $this->makeFreeExternalLink( $m[0] );
} elseif ( isset( $m[4] ) && $m[4] !== '' ) {
# RFC or PMID
+ $CssClass = '';
if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
$keyword = 'RFC';
$urlmsg = 'rfcurl';
+ $CssClass = 'mw-magiclink-rfc';
$id = $m[4];
} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
$keyword = 'PMID';
$urlmsg = 'pubmedurl';
+ $CssClass = 'mw-magiclink-pmid';
$id = $m[4];
} else {
throw new MWException( __METHOD__.': unrecognised match type "' .
@@ -958,7 +1019,7 @@ class Parser
}
$url = wfMsg( $urlmsg, $id);
$sk = $this->mOptions->getSkin();
- $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+ $la = $sk->getExternalLinkAttributes( "external $CssClass" );
return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
} elseif ( isset( $m[5] ) && $m[5] !== '' ) {
# ISBN
@@ -971,7 +1032,7 @@ class Parser
$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
return'<a href="' .
$titleObj->escapeLocalUrl() .
- "\" class=\"internal\">ISBN $isbn</a>";
+ "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
} else {
return $m[0];
}
@@ -1017,7 +1078,7 @@ class Parser
$text = $this->maybeMakeExternalImage( $url );
if ( $text === false ) {
# Not an image, make a link
- $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
+ $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
$this->getExternalLinkAttribs( $url ) );
# Register it in the output object...
# Replace unnecessary URL escape codes with their equivalent characters
@@ -1457,7 +1518,7 @@ class Parser
wfProfileIn( __METHOD__.'-setup' );
static $tc = FALSE, $e1, $e1_img;
# the % is needed to support urlencoded titles as well
- if ( !$tc ) {
+ if ( !$tc ) {
$tc = Title::legalChars() . '#%';
# Match a link having the form [[namespace:link|alternate]]trail
$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
@@ -1581,29 +1642,29 @@ class Parser
# 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])) {
+ if ( preg_match( '/^\b(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
$s .= $prefix . '[[' . $line ;
wfProfileOut( __METHOD__."-misc" );
continue;
}
# Make subpage if necessary
- if( $useSubpages ) {
+ if ( $useSubpages ) {
$link = $this->maybeDoSubpageLink( $m[1], $text );
} else {
$link = $m[1];
}
- $noforce = (substr($m[1], 0, 1) !== ':');
+ $noforce = (substr( $m[1], 0, 1 ) !== ':');
if (!$noforce) {
# Strip off leading ':'
- $link = substr($link, 1);
+ $link = substr( $link, 1 );
}
wfProfileOut( __METHOD__."-misc" );
wfProfileIn( __METHOD__."-title" );
- $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
- if( $nt === NULL ) {
+ $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
+ if ( $nt === null ) {
$s .= $prefix . '[[' . $line;
wfProfileOut( __METHOD__."-title" );
continue;
@@ -1613,9 +1674,9 @@ class Parser
$iw = $nt->getInterWiki();
wfProfileOut( __METHOD__."-title" );
- if ($might_be_img) { # if this is actually an invalid link
+ if ( $might_be_img ) { # if this is actually an invalid link
wfProfileIn( __METHOD__."-might_be_img" );
- if ($ns == NS_FILE && $noforce) { #but might be an image
+ if ( $ns == NS_FILE && $noforce ) { #but might be an image
$found = false;
while ( true ) {
#look at the next 'line' to see if we can close it there
@@ -1658,15 +1719,15 @@ class Parser
wfProfileOut( __METHOD__."-might_be_img" );
}
- $wasblank = ( '' == $text );
- if( $wasblank ) $text = $link;
+ $wasblank = ( $text == '' );
+ if ( $wasblank ) $text = $link;
# Link not escaped by : , create the various objects
- if( $noforce ) {
+ if ( $noforce ) {
# Interwikis
wfProfileIn( __METHOD__."-interwiki" );
- if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+ if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
$this->mOutput->addLanguageLink( $nt->getFullText() );
$s = rtrim($s . $prefix);
$s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
@@ -1678,14 +1739,23 @@ class Parser
if ( $ns == NS_FILE ) {
wfProfileIn( __METHOD__."-image" );
if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
- # recursively parse links inside the image caption
- # actually, this will parse them in any other parameters, too,
- # but it might be hard to fix that, and it doesn't matter ATM
- $text = $this->replaceExternalLinks($text);
- $holders->merge( $this->replaceInternalLinks2( $text ) );
-
+ if ( $wasblank ) {
+ # if no parameters were passed, $text
+ # becomes something like "File:Foo.png",
+ # which we don't want to pass on to the
+ # image generator
+ $text = '';
+ } else {
+ # 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);
+ $holders->merge( $this->replaceInternalLinks2( $text ) );
+ }
# cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
$s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
+ } else {
+ $s .= $prefix . $trail;
}
$this->mOutput->addImage( $nt->getDBkey() );
wfProfileOut( __METHOD__."-image" );
@@ -1793,6 +1863,7 @@ class Parser
function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
list( $inside, $trail ) = Linker::splitTrail( $trail );
$sk = $this->mOptions->getSkin();
+ // FIXME: use link() instead of deprecated makeKnownLinkObj()
$link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
return $this->armorLinks( $link ) . $trail;
}
@@ -1829,75 +1900,7 @@ class Parser
* @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
-
- wfProfileIn( __METHOD__ );
- $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( __METHOD__ );
- return $ret;
+ return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
}
/**#@+
@@ -1906,7 +1909,7 @@ class Parser
*/
/* private */ function closeParagraph() {
$result = '';
- if ( '' != $this->mLastSection ) {
+ if ( $this->mLastSection != '' ) {
$result = '</' . $this->mLastSection . ">\n";
}
$this->mInPre = false;
@@ -1933,9 +1936,9 @@ class Parser
$result = $this->closeParagraph();
if ( '*' === $char ) { $result .= '<ul><li>'; }
- else if ( '#' === $char ) { $result .= '<ol><li>'; }
- else if ( ':' === $char ) { $result .= '<dl><dd>'; }
- else if ( ';' === $char ) {
+ elseif ( '#' === $char ) { $result .= '<ol><li>'; }
+ elseif ( ':' === $char ) { $result .= '<dl><dd>'; }
+ elseif ( ';' === $char ) {
$result .= '<dl><dt>';
$this->mDTopen = true;
}
@@ -1946,7 +1949,7 @@ class Parser
/* private */ function nextItem( $char ) {
if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
- else if ( ':' === $char || ';' === $char ) {
+ elseif ( ':' === $char || ';' === $char ) {
$close = '</dd>';
if ( $this->mDTopen ) { $close = '</dt>'; }
if ( ';' === $char ) {
@@ -1962,8 +1965,8 @@ class Parser
/* private */ function closeList( $char ) {
if ( '*' === $char ) { $text = '</li></ul>'; }
- else if ( '#' === $char ) { $text = '</li></ol>'; }
- else if ( ':' === $char ) {
+ elseif ( '#' === $char ) { $text = '</li></ol>'; }
+ elseif ( ':' === $char ) {
if ( $this->mDTopen ) {
$this->mDTopen = false;
$text = '</dt></dl>';
@@ -1979,6 +1982,7 @@ class Parser
/**
* Make lists from lines starting with ':', '*', '#', etc. (DBL)
*
+ * @param $linestart bool whether or not this is at the start of a line.
* @private
* @return string the lists rendered as HTML
*/
@@ -2003,16 +2007,24 @@ class Parser
$linestart = true;
continue;
}
+ // * = ul
+ // # = ol
+ // ; = dt
+ // : = dd
$lastPrefixLength = strlen( $lastPrefix );
$preCloseMatch = preg_match('/<\\/pre/i', $oLine );
$preOpenMatch = preg_match('/<pre/i', $oLine );
+ // If not in a <pre> element, scan for and figure out what prefixes are there.
if ( !$this->mInPre ) {
# Multiple prefixes may abut each other for nested lists.
$prefixLength = strspn( $oLine, '*#:;' );
$prefix = substr( $oLine, 0, $prefixLength );
# eh?
+ // ; and : are both from definition-lists, so they're equivalent
+ // for the purposes of determining whether or not we need to open/close
+ // elements.
$prefix2 = str_replace( ';', ':', $prefix );
$t = substr( $oLine, $prefixLength );
$this->mInPre = (bool)$preOpenMatch;
@@ -2041,17 +2053,24 @@ class Parser
}
}
} elseif( $prefixLength || $lastPrefixLength ) {
+ // We need to open or close prefixes, or both.
+
# Either open or close a level...
$commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
$paragraphStack = false;
+ // Close all the prefixes which aren't shared.
while( $commonPrefixLength < $lastPrefixLength ) {
$output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
--$lastPrefixLength;
}
+
+ // Continue the current prefix if appropriate.
if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
$output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
}
+
+ // Open prefixes where appropriate.
while ( $prefixLength > $commonPrefixLength ) {
$char = substr( $prefix, $commonPrefixLength, 1 );
$output .= $this->openList( $char );
@@ -2067,6 +2086,8 @@ class Parser
}
$lastPrefix = $prefix2;
}
+
+ // If we have no prefixes, go to paragraph mode.
if( 0 == $prefixLength ) {
wfProfileIn( __METHOD__."-paragraph" );
# No prefix (not in list)--go to paragraph mode
@@ -2098,7 +2119,7 @@ class Parser
$t = substr( $t, 1 );
} else {
// paragraph
- if ( '' == trim($t) ) {
+ if ( trim($t) == '' ) {
if ( $paragraphStack ) {
$output .= $paragraphStack.'<br />';
$paragraphStack = false;
@@ -2138,7 +2159,7 @@ class Parser
$output .= $this->closeList( $prefix2[$prefixLength-1] );
--$prefixLength;
}
- if ( '' != $this->mLastSection ) {
+ if ( $this->mLastSection != '' ) {
$output .= '</' . $this->mLastSection . '>';
$this->mLastSection = '';
}
@@ -2315,8 +2336,9 @@ class Parser
*
* @private
*/
- function getVariableValue( $index ) {
- global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
+ function getVariableValue( $index, $frame=false ) {
+ global $wgContLang, $wgSitename, $wgServer, $wgServerName;
+ global $wgScriptPath, $wgStylePath;
/**
* Some of these require message or data lookups and can be
@@ -2334,13 +2356,13 @@ class Parser
# Use the time zone
global $wgLocaltimezone;
if ( isset( $wgLocaltimezone ) ) {
- $oldtz = getenv( 'TZ' );
- putenv( 'TZ='.$wgLocaltimezone );
+ $oldtz = date_default_timezone_get();
+ date_default_timezone_set( $wgLocaltimezone );
}
- wfSuppressWarnings(); // E_STRICT system time bitching
$localTimestamp = date( 'YmdHis', $ts );
$localMonth = date( 'm', $ts );
+ $localMonth1 = date( 'n', $ts );
$localMonthName = date( 'n', $ts );
$localDay = date( 'j', $ts );
$localDay2 = date( 'd', $ts );
@@ -2349,175 +2371,240 @@ class Parser
$localYear = date( 'Y', $ts );
$localHour = date( 'H', $ts );
if ( isset( $wgLocaltimezone ) ) {
- putenv( 'TZ='.$oldtz );
+ date_default_timezone_set( $oldtz );
}
- wfRestoreWarnings();
switch ( $index ) {
case 'currentmonth':
- return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+ $value = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+ break;
+ case 'currentmonth1':
+ $value = $wgContLang->formatNum( gmdate( 'n', $ts ) );
+ break;
case 'currentmonthname':
- return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+ $value = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+ break;
case 'currentmonthnamegen':
- return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+ $value = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+ break;
case 'currentmonthabbrev':
- return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+ $value = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+ break;
case 'currentday':
- return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+ $value = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+ break;
case 'currentday2':
- return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+ $value = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+ break;
case 'localmonth':
- return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
+ $value = $wgContLang->formatNum( $localMonth );
+ break;
+ case 'localmonth1':
+ $value = $wgContLang->formatNum( $localMonth1 );
+ break;
case 'localmonthname':
- return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
+ $value = $wgContLang->getMonthName( $localMonthName );
+ break;
case 'localmonthnamegen':
- return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
+ $value = $wgContLang->getMonthNameGen( $localMonthName );
+ break;
case 'localmonthabbrev':
- return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
+ $value = $wgContLang->getMonthAbbreviation( $localMonthName );
+ break;
case 'localday':
- return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
+ $value = $wgContLang->formatNum( $localDay );
+ break;
case 'localday2':
- return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 );
+ $value = $wgContLang->formatNum( $localDay2 );
+ break;
case 'pagename':
- return wfEscapeWikiText( $this->mTitle->getText() );
+ $value = wfEscapeWikiText( $this->mTitle->getText() );
+ break;
case 'pagenamee':
- return $this->mTitle->getPartialURL();
+ $value = $this->mTitle->getPartialURL();
+ break;
case 'fullpagename':
- return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+ $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+ break;
case 'fullpagenamee':
- return $this->mTitle->getPrefixedURL();
+ $value = $this->mTitle->getPrefixedURL();
+ break;
case 'subpagename':
- return wfEscapeWikiText( $this->mTitle->getSubpageText() );
+ $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
+ break;
case 'subpagenamee':
- return $this->mTitle->getSubpageUrlForm();
+ $value = $this->mTitle->getSubpageUrlForm();
+ break;
case 'basepagename':
- return wfEscapeWikiText( $this->mTitle->getBaseText() );
+ $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
+ break;
case 'basepagenamee':
- return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+ $value = wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+ break;
case 'talkpagename':
if( $this->mTitle->canTalk() ) {
$talkPage = $this->mTitle->getTalkPage();
- return wfEscapeWikiText( $talkPage->getPrefixedText() );
+ $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
} else {
- return '';
+ $value = '';
}
+ break;
case 'talkpagenamee':
if( $this->mTitle->canTalk() ) {
$talkPage = $this->mTitle->getTalkPage();
- return $talkPage->getPrefixedUrl();
+ $value = $talkPage->getPrefixedUrl();
} else {
- return '';
+ $value = '';
}
+ break;
case 'subjectpagename':
$subjPage = $this->mTitle->getSubjectPage();
- return wfEscapeWikiText( $subjPage->getPrefixedText() );
+ $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
+ break;
case 'subjectpagenamee':
$subjPage = $this->mTitle->getSubjectPage();
- return $subjPage->getPrefixedUrl();
+ $value = $subjPage->getPrefixedUrl();
+ break;
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;
+ $value = $this->mRevisionId;
+ break;
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 ) );
+ $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+ break;
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 );
+ $value = substr( $this->getRevisionTimestamp(), 6, 2 );
+ break;
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 ) );
+ $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+ break;
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 );
+ $value = substr( $this->getRevisionTimestamp(), 0, 4 );
+ break;
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();
+ $value = $this->getRevisionTimestamp();
+ break;
case 'revisionuser':
// 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__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
- return $this->getRevisionUser();
+ $value = $this->getRevisionUser();
+ break;
case 'namespace':
- return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ $value = str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ break;
case 'namespacee':
- return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+ break;
case 'talkspace':
- return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
+ $value = $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
+ break;
case 'talkspacee':
- return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+ $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+ break;
case 'subjectspace':
- return $this->mTitle->getSubjectNsText();
+ $value = $this->mTitle->getSubjectNsText();
+ break;
case 'subjectspacee':
- return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+ $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+ break;
case 'currentdayname':
- return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+ $value = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+ break;
case 'currentyear':
- return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+ $value = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+ break;
case 'currenttime':
- return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+ $value = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+ break;
case 'currenthour':
- return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+ $value = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+ break;
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 ) );
+ $value = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+ break;
case 'currentdow':
- return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+ $value = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+ break;
case 'localdayname':
- return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+ $value = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+ break;
case 'localyear':
- return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
+ $value = $wgContLang->formatNum( $localYear, true );
+ break;
case 'localtime':
- return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
+ $value = $wgContLang->time( $localTimestamp, false, false );
+ break;
case 'localhour':
- return $this->mVarCache[$index] = $wgContLang->formatNum( $localHour, true );
+ $value = $wgContLang->formatNum( $localHour, true );
+ break;
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 );
+ $value = $wgContLang->formatNum( (int)$localWeek );
+ break;
case 'localdow':
- return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
+ $value = $wgContLang->formatNum( $localDayOfWeek );
+ break;
case 'numberofarticles':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
+ $value = $wgContLang->formatNum( SiteStats::articles() );
+ break;
case 'numberoffiles':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
+ $value = $wgContLang->formatNum( SiteStats::images() );
+ break;
case 'numberofusers':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+ $value = $wgContLang->formatNum( SiteStats::users() );
+ break;
case 'numberofactiveusers':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::activeUsers() );
+ $value = $wgContLang->formatNum( SiteStats::activeUsers() );
+ break;
case 'numberofpages':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
+ $value = $wgContLang->formatNum( SiteStats::pages() );
+ break;
case 'numberofadmins':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
+ $value = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
+ break;
case 'numberofedits':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+ $value = $wgContLang->formatNum( SiteStats::edits() );
+ break;
case 'numberofviews':
- return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() );
+ $value = $wgContLang->formatNum( SiteStats::views() );
+ break;
case 'currenttimestamp':
- return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
+ $value = wfTimestamp( TS_MW, $ts );
+ break;
case 'localtimestamp':
- return $this->mVarCache[$index] = $localTimestamp;
+ $value = $localTimestamp;
+ break;
case 'currentversion':
- return $this->mVarCache[$index] = SpecialVersion::getVersion();
+ $value = SpecialVersion::getVersion();
+ break;
case 'sitename':
return $wgSitename;
case 'server':
@@ -2526,6 +2613,8 @@ class Parser
return $wgServerName;
case 'scriptpath':
return $wgScriptPath;
+ case 'stylepath':
+ return $wgStylePath;
case 'directionmark':
return $wgContLang->getDirMark();
case 'contentlanguage':
@@ -2533,23 +2622,30 @@ class Parser
return $wgContLanguageCode;
default:
$ret = null;
- if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) )
+ if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) ) )
return $ret;
else
return null;
}
+
+ if ( $index )
+ $this->mVarCache[$index] = $value;
+
+ return $value;
}
/**
- * initialise the magic variables (like CURRENTMONTHNAME)
+ * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
*
* @private
*/
function initialiseVariables() {
wfProfileIn( __METHOD__ );
$variableIDs = MagicWord::getVariableIDs();
+ $substIDs = MagicWord::getSubstIDs();
$this->mVariables = new MagicWordArray( $variableIDs );
+ $this->mSubstWords = new MagicWordArray( $substIDs );
wfProfileOut( __METHOD__ );
}
@@ -2607,7 +2703,7 @@ class Parser
* 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.
+ * @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
@@ -2670,14 +2766,10 @@ class Parser
* 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 = wfMsgExt( $msgName, array( 'parsemag', 'escape' ), $current, $max );
+ $warning = wfMsgExt( "$limitationType-warning", array( 'parsemag', 'escape' ), $current, $max );
$this->mOutput->addWarning( $warning );
- $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
- if ( $cat ) {
- $this->mOutput->addCategory( $cat->getDBkey(), $this->getDefaultSort() );
- }
+ $this->addTrackingCategory( "$limitationType-category" );
}
/**
@@ -2706,7 +2798,7 @@ class Parser
$isLocalObj = false; # $text is a DOM node needing expansion in the current frame
# Title object, where $text came from
- $title = NULL;
+ $title = null;
# $part1 is the bit before the first |, and must contain only title characters.
# Various prefixes will be stripped from it later.
@@ -2724,12 +2816,25 @@ class Parser
# 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
+
+ $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
+
+ # Possibilities for substMatch: "subst", "safesubst" or FALSE
+ # Decide whether to expand template or keep wikitext as-is.
+ if ( $this->ot['wiki'] ) {
+ if ( $substMatch === false ) {
+ $literal = true; # literal when in PST with no prefix
+ } else {
+ $literal = false; # expand when in PST with subst: or safesubst:
+ }
+ } else {
+ if ( $substMatch == 'subst' ) {
+ $literal = true; # literal when not in PST with plain subst:
+ } else {
+ $literal = false; # expand when not in PST with safesubst: or no prefix
+ }
+ }
+ if ( $literal ) {
$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
$isLocalObj = true;
$found = true;
@@ -2740,7 +2845,7 @@ class Parser
if ( !$found && $args->getLength() == 0 ) {
$id = $this->mVariables->matchStartToEnd( $part1 );
if ( $id !== false ) {
- $text = $this->getVariableValue( $id );
+ $text = $this->getVariableValue( $id, $frame );
if (MagicWord::getCacheTTL($id)>-1)
$this->mOutput->mContainsOldMagic = true;
$found = true;
@@ -2779,7 +2884,7 @@ class Parser
$function = $this->mFunctionSynonyms[1][$function];
} else {
# Case insensitive functions
- $function = strtolower( $function );
+ $function = $wgContLang->lc( $function );
if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
$function = $this->mFunctionSynonyms[0][$function];
} else {
@@ -2808,13 +2913,15 @@ class Parser
# Workaround for PHP bug 35229 and similar
if ( !is_callable( $callback ) ) {
+ wfProfileOut( __METHOD__ . '-pfunc' );
+ wfProfileOut( __METHOD__ );
throw new MWException( "Tag hook for $function 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];
@@ -3118,14 +3225,11 @@ class Parser
function fetchScaryTemplateMaybeFromCache($url) {
global $wgTranscludeCacheExpiry;
$dbr = wfGetDB(DB_SLAVE);
+ $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
$obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
- array('tc_url' => $url));
+ array('tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
if ($obj) {
- $time = $obj->tc_time;
- $text = $obj->tc_contents;
- if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
- return $text;
- }
+ return $obj->tc_contents;
}
$text = Http::get($url);
@@ -3135,7 +3239,7 @@ class Parser
$dbw = wfGetDB(DB_MASTER);
$dbw->replace('transcache', array('tc_url'), array(
'tc_url' => $url,
- 'tc_time' => time(),
+ 'tc_time' => $dbw->timestamp( time() ),
'tc_contents' => $text));
return $text;
}
@@ -3204,47 +3308,47 @@ class Parser
$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'] ) {
+ $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower($name)] ) &&
+ ( $this->ot['html'] || $this->ot['pre'] );
+ if ( $isFunctionTag ) {
+ $markerType = 'none';
+ } else {
+ $markerType = 'general';
+ }
+ if ( $this->ot['html'] || $isFunctionTag ) {
$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':
- $content = strtr($content, array('-{' => '-&#123;', '}-' => '&#125;-'));
- $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>';
- }
+
+ 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, $frame ) );
+ } elseif( isset( $this->mFunctionTagHooks[$name] ) ) {
+ list( $callback, $flags ) = $this->mFunctionTagHooks[$name];
+ if( !is_callable( $callback ) )
+ throw new MWException( "Tag hook for $name is not callable\n" );
+
+ $output = call_user_func_array( $callback,
+ array( &$this, $frame, $content, $attributes ) );
+ } else {
+ $output = '<span class="error">Invalid tag extension name: ' .
+ htmlspecialchars( $name ) . '</span>';
+ }
+
+ if ( is_array( $output ) ) {
+ // Extract flags to local scope (to override $markerType)
+ $flags = $output;
+ $output = $flags[0];
+ unset( $flags[0] );
+ extract( $flags );
}
} else {
if ( is_null( $attrText ) ) {
@@ -3264,10 +3368,14 @@ class Parser
}
}
- if ( $name === 'html' || $name === 'nowiki' ) {
+ if( $markerType === 'none' ) {
+ return $output;
+ } elseif ( $markerType === 'nowiki' ) {
$this->mStripState->nowiki->setPair( $marker, $output );
- } else {
+ } elseif ( $markerType === 'general' ) {
$this->mStripState->general->setPair( $marker, $output );
+ } else {
+ throw new MWException( __METHOD__.': invalid marker type' );
}
return $marker;
}
@@ -3308,6 +3416,7 @@ class Parser
*/
function doDoubleUnderscore( $text ) {
wfProfileIn( __METHOD__ );
+
// The position of __TOC__ needs to be recorded
$mw = MagicWord::get( 'toc' );
if( $mw->match( $text ) ) {
@@ -3333,28 +3442,48 @@ class Parser
}
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" );
- }
+ $this->addTrackingCategory( 'hidden-category-category' );
}
# (bug 8068) Allow control over whether robots index a page.
#
# FIXME (bug 14899): __INDEX__ always overrides __NOINDEX__ here! This
# is not desirable, the last one on the page should win.
- if( isset( $this->mDoubleUnderscores['noindex'] ) ) {
+ if( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
$this->mOutput->setIndexPolicy( 'noindex' );
- } elseif( isset( $this->mDoubleUnderscores['index'] ) ) {
+ $this->addTrackingCategory( 'noindex-category' );
+ }
+ if( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ){
$this->mOutput->setIndexPolicy( 'index' );
+ $this->addTrackingCategory( 'index-category' );
}
+
wfProfileOut( __METHOD__ );
return $text;
}
/**
+ * Add a tracking category, getting the title from a system message,
+ * or print a debug message if the title is invalid.
+ * @param $msg String message key
+ * @return Bool whether the addition was successful
+ */
+ protected function addTrackingCategory( $msg ){
+ $cat = wfMsgForContent( $msg );
+
+ # Allow tracking categories to be disabled by setting them to "-"
+ if( $cat === '-' ) return false;
+
+ $containerCategory = Title::makeTitleSafe( NS_CATEGORY, $cat );
+ if ( $containerCategory ) {
+ $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
+ return true;
+ } else {
+ wfDebug( __METHOD__.": [[MediaWiki:$msg]] is not a valid title!\n" );
+ return false;
+ }
+ }
+
+ /**
* 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
@@ -3365,11 +3494,12 @@ class Parser
* string and re-inserts the newly formatted headlines.
*
* @param string $text
+ * @param string $origText Original, untouched wikitext
* @param boolean $isMain
* @private
*/
- function formatHeadings( $text, $isMain=true ) {
- global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds;
+ function formatHeadings( $text, $origText, $isMain=true ) {
+ global $wgMaxTocLevel, $wgContLang, $wgHtml5, $wgExperimentalHtmlIds;
$doNumberHeadings = $this->mOptions->getNumberHeadings();
$showEditLink = $this->mOptions->getEditSection();
@@ -3434,6 +3564,12 @@ class Parser
$prevtoclevel = 0;
$markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
$baseTitleText = $this->mTitle->getPrefixedDBkey();
+ $oldType = $this->mOutputType;
+ $this->setOutputType( self::OT_WIKI );
+ $frame = $this->getPreprocessor()->newFrame();
+ $root = $this->preprocessToDom( $origText );
+ $node = $root->getFirstChild();
+ $byteOffset = 0;
$tocraw = array();
foreach( $matches[3] as $headline ) {
@@ -3455,68 +3591,61 @@ class Parser
}
$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++;
- }
+ 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
+ }
+ 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;
- }
- }
+ for ($i = $toclevel; $i > 0; $i--) {
+ if ( $levelCount[$i] == $level ) {
+ # Found last matching level
+ $toclevel = $i;
+ 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();
- }
+ elseif ( $levelCount[$i] < $level ) {
+ # Found first matching level below current level
+ $toclevel = $i + 1;
+ break;
}
}
- else {
- # No change in level, end TOC line
- if( $toclevel<$wgMaxTocLevel ) {
+ if( $i == 0 ) $toclevel = 1;
+ 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;
+ $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;
+ # 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;
}
}
@@ -3540,16 +3669,13 @@ class Parser
# For the anchor, strip out HTML-y stuff period
$safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
+ $safeHeadline = preg_replace( '/[ _]+/', ' ', $safeHeadline );
$safeHeadline = trim( $safeHeadline );
# Save headline for section edit hint before it's escaped
$headlineHint = $safeHeadline;
- if ( $wgEnforceHtmlIds ) {
- $legacyHeadline = false;
- $safeHeadline = Sanitizer::escapeId( $safeHeadline,
- 'noninitial' );
- } else {
+ if ( $wgHtml5 && $wgExperimentalHtmlIds ) {
# For reverse compatibility, provide an id that's
# HTML4-compatible, like we used to.
#
@@ -3561,20 +3687,17 @@ class Parser
# to type in section names like "abc_.D7.93.D7.90.D7.A4"
# manually, so let's not bother worrying about it.
$legacyHeadline = Sanitizer::escapeId( $safeHeadline,
- 'noninitial' );
- $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' );
+ array( 'noninitial', 'legacy' ) );
+ $safeHeadline = Sanitizer::escapeId( $safeHeadline );
if ( $legacyHeadline == $safeHeadline ) {
# No reason to have both (in fact, we can't)
$legacyHeadline = false;
- } elseif ( $legacyHeadline != Sanitizer::escapeId(
- $legacyHeadline, 'xml' ) ) {
- # The legacy id is invalid XML. We used to allow this, but
- # there's no reason to do so anymore. Backward
- # compatibility will fail slightly in this case, but it's
- # no big deal.
- $legacyHeadline = false;
}
+ } else {
+ $legacyHeadline = false;
+ $safeHeadline = Sanitizer::escapeId( $safeHeadline,
+ 'noninitial' );
}
# HTML names must be case-insensitively unique (bug 10721). FIXME:
@@ -3602,7 +3725,7 @@ class Parser
# 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;
+ $headline = $numbering . ' ' . $headline;
}
# Create the anchor for linking from the TOC to the section
@@ -3615,9 +3738,33 @@ class Parser
$legacyAnchor .= '_' . $refers[$legacyArrayKey];
}
if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
- $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
- $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
+ $toc .= $sk->tocLine($anchor, $tocline,
+ $numbering, $toclevel, ($isTemplate ? false : $sectionIndex));
+ }
+
+ # Add the section to the section tree
+ # Find the DOM node for this header
+ while ( $node && !$isTemplate ) {
+ if ( $node->getName() === 'h' ) {
+ $bits = $node->splitHeading();
+ if ( $bits['i'] == $sectionIndex )
+ break;
+ }
+ $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
+ $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
+ $node = $node->getNextSibling();
}
+ $tocraw[] = array(
+ 'toclevel' => $toclevel,
+ 'level' => $level,
+ 'line' => $tocline,
+ 'number' => $numbering,
+ 'index' => ($isTemplate ? 'T-' : '' ) . $sectionIndex,
+ 'fromtitle' => $titleText,
+ 'byteoffset' => ( $isTemplate ? null : $byteOffset ),
+ 'anchor' => $anchor,
+ );
+
# give headline the correct <h#> tag
if( $showEditLink && $sectionIndex !== false ) {
if( $isTemplate ) {
@@ -3637,7 +3784,7 @@ class Parser
$headlineCount++;
}
- $this->mOutput->setSections( $tocraw );
+ $this->setOutputType( $oldType );
# Never ever show TOC if no headers
if( $numVisible < 1 ) {
@@ -3649,6 +3796,11 @@ class Parser
$toc .= $sk->tocUnindent( $prevtoclevel - 1 );
}
$toc = $sk->tocList( $toc );
+ $this->mOutput->setTOCHTML( $toc );
+ }
+
+ if ( $isMain ) {
+ $this->mOutput->setSections( $tocraw );
}
# split up and insert constructed headlines
@@ -3684,6 +3836,96 @@ class Parser
}
/**
+ * Merge $tree2 into $tree1 by replacing the section with index
+ * $section in $tree1 and its descendants with the sections in $tree2.
+ * Note that in the returned section tree, only the 'index' and
+ * 'byteoffset' fields are guaranteed to be correct.
+ * @param $tree1 array Section tree from ParserOutput::getSectons()
+ * @param $tree2 array Section tree
+ * @param $section int Section index
+ * @param $title Title Title both section trees come from
+ * @param $len2 int Length of the original wikitext for $tree2
+ * @return array Merged section tree
+ */
+ public static function mergeSectionTrees( $tree1, $tree2, $section, $title, $len2 ) {
+ global $wgContLang;
+ $newTree = array();
+ $targetLevel = false;
+ $merged = false;
+ $lastLevel = 1;
+ $nextIndex = 1;
+ $numbering = array( 0 );
+ $titletext = $title->getPrefixedDBkey();
+ foreach ( $tree1 as $s ) {
+ if ( $targetLevel !== false ) {
+ if ( $s['level'] <= $targetLevel )
+ // We've skipped enough
+ $targetLevel = false;
+ else
+ continue;
+ }
+ if ( $s['index'] != $section ||
+ $s['fromtitle'] != $titletext ) {
+ self::incrementNumbering( $numbering,
+ $s['toclevel'], $lastLevel );
+
+ // Rewrite index, byteoffset and number
+ if ( $s['fromtitle'] == $titletext ) {
+ $s['index'] = $nextIndex++;
+ if ( $merged )
+ $s['byteoffset'] += $len2;
+ }
+ $s['number'] = implode( '.', array_map(
+ array( $wgContLang, 'formatnum' ),
+ $numbering ) );
+ $lastLevel = $s['toclevel'];
+ $newTree[] = $s;
+ } else {
+ // We're at $section
+ // Insert sections from $tree2 here
+ foreach ( $tree2 as $s2 ) {
+ // Rewrite the fields in $s2
+ // before inserting it
+ $s2['toclevel'] += $s['toclevel'] - 1;
+ $s2['level'] += $s['level'] - 1;
+ $s2['index'] = $nextIndex++;
+ $s2['byteoffset'] += $s['byteoffset'];
+
+ self::incrementNumbering( $numbering,
+ $s2['toclevel'], $lastLevel );
+ $s2['number'] = implode( '.', array_map(
+ array( $wgContLang, 'formatnum' ),
+ $numbering ) );
+ $lastLevel = $s2['toclevel'];
+ $newTree[] = $s2;
+ }
+ // Skip all descendants of $section in $tree1
+ $targetLevel = $s['level'];
+ $merged = true;
+ }
+ }
+ return $newTree;
+ }
+
+ /**
+ * Increment a section number. Helper function for mergeSectionTrees()
+ * @param $number array Array representing a section number
+ * @param $level int Current TOC level (depth)
+ * @param $lastLevel int Level of previous TOC entry
+ */
+ private static function incrementNumbering( &$number, $level, $lastLevel ) {
+ if ( $level > $lastLevel )
+ $number[$level - 1] = 1;
+ else if ( $level < $lastLevel ) {
+ foreach ( $number as $key => $unused )
+ if ( $key >= $level )
+ unset( $number[$key] );
+ $number[$level - 1]++;
+ } else
+ $number[$level - 1]++;
+ }
+
+ /**
* Transform wiki markup when saving a page by doing \r\n -> \n
* conversion, substitting signatures, {{subst:}} templates, etc.
*
@@ -3728,26 +3970,29 @@ class Parser
* (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!
+ $tz = $wgLocaltimezone;
+ } else {
+ $tz = date_default_timezone_get();
+ }
- /* Allow translation of timezones trough wiki. date() can return
- * whatever crap the system uses, localised or not, so we cannot
- * ship premade translations.
- */
- $key = 'timezone-' . strtolower( trim( $tz ) );
- $value = wfMsgForContent( $key );
- if ( !wfEmptyMsg( $key, $value ) ) $tz = $value;
+ $unixts = wfTimestamp( TS_UNIX, $ts );
+ $oldtz = date_default_timezone_get();
+ date_default_timezone_set( $tz );
+ $ts = date( 'YmdHis', $unixts );
+ $tzMsg = date( 'T', $unixts ); # might vary on DST changeover!
- putenv( 'TZ='.$oldtz );
- }
+ /* Allow translation of timezones trough wiki. date() can return
+ * whatever crap the system uses, localised or not, so we cannot
+ * ship premade translations.
+ */
+ $key = 'timezone-' . strtolower( trim( $tzMsg ) );
+ $value = wfMsgForContent( $key );
+ if ( !wfEmptyMsg( $key, $value ) ) $tzMsg = $value;
+
+ date_default_timezone_set( $oldtz );
- $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)";
+ $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
# Variable replacement
# Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
@@ -3781,7 +4026,7 @@ class Parser
$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]" ) {
+ } 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
@@ -3797,22 +4042,30 @@ class Parser
/**
* Fetch the user's signature text, if any, and normalize to
* validated, ready-to-insert wikitext.
+ * If you have pre-fetched the nickname or the fancySig option, you can
+ * specify them here to save a database query.
*
* @param User $user
* @return string
- * @private
*/
- function getUserSig( &$user ) {
+ function getUserSig( &$user, $nickname = false, $fancySig = null ) {
global $wgMaxSigChars;
$username = $user->getName();
- $nickname = $user->getOption( 'nickname' );
- $nickname = $nickname === '' ? $username : $nickname;
+
+ // If not given, retrieve from the user object.
+ if ( $nickname === false )
+ $nickname = $user->getOption( 'nickname' );
+
+ if ( is_null( $fancySig ) )
+ $fancySig = $user->getBoolOption( 'fancysig' );
+
+ $nickname = $nickname == null ? $username : $nickname;
if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
$nickname = $username;
wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
- } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
+ } elseif( $fancySig !== false ) {
# Sig. might contain markup; validate this
if( $this->validateSig( $nickname ) !== false ) {
# Validated; clean up (if needed) and return it
@@ -4005,28 +4258,30 @@ class Parser
* @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
+ * 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
+ * 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
+ * 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
+ * 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 ) {
+ global $wgContLang;
+
$oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
$this->mFunctionHooks[$id] = array( $callback, $flags );
@@ -4041,7 +4296,7 @@ class Parser
foreach ( $synonyms as $syn ) {
# Case
if ( !$sensitive ) {
- $syn = strtolower( $syn );
+ $syn = $wgContLang->lc( $syn );
}
# Add leading hash
if ( !( $flags & SFH_NO_HASH ) ) {
@@ -4066,6 +4321,25 @@ class Parser
}
/**
+ * Create a tag function, e.g. <test>some stuff</test>.
+ * Unlike tag hooks, tag functions are parsed at preprocessor level.
+ * Unlike parser functions, their content is not preprocessed.
+ */
+ function setFunctionTagHook( $tag, $callback, $flags ) {
+ $tag = strtolower( $tag );
+ $old = isset( $this->mFunctionTagHooks[$tag] ) ?
+ $this->mFunctionTagHooks[$tag] : null;
+ $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
+
+ if( !in_array( $tag, $this->mStripList ) ) {
+ $this->mStripList[] = $tag;
+ }
+
+ return $old;
+ }
+
+ /**
+ * FIXME: update documentation. makeLinkObj() is deprecated.
* 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.
@@ -4085,19 +4359,6 @@ class Parser
}
/**
- * 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 Xml::openElement( '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"
@@ -4145,7 +4406,7 @@ class Parser
if ( count( $matches ) == 0 ) {
continue;
}
-
+
if ( strpos( $matches[0], '%' ) !== false )
$matches[1] = urldecode( $matches[1] );
$tp = Title::newFromText( $matches[1]/*, NS_FILE*/ );
@@ -4227,11 +4488,13 @@ class Parser
# * 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.
+ # * frame Keep original image size, no magnify-button.
+ # * framed Same as "frame"
# * frameless like 'thumb' but without a frame. Keeps user preferences for width
# * upright reduce width for upright images, rounded to full __0 px
# * border draw a 1px border around the image
# * alt Text for HTML alt attribute (defaults to empty)
+ # * link Set the target of the image link. Can be external, interwiki, or local
# vertical-align values (no % or length right now):
# * baseline
# * sub
@@ -4255,15 +4518,7 @@ class Parser
# Get the file
$imagename = $title->getDBkey();
- if ( isset( $this->mFileCache[$imagename][$time] ) ) {
- $file = $this->mFileCache[$imagename][$time];
- } else {
- $file = wfFindFile( $title, $time );
- if ( count( $this->mFileCache ) > 1000 ) {
- $this->mFileCache = array();
- }
- $this->mFileCache[$imagename][$time] = $file;
- }
+ $file = wfFindFile( $title, array( 'time' => $time ) );
# Get parameter map
$handler = $file ? $file->getHandler() : false;
@@ -4312,7 +4567,7 @@ class Parser
switch( $paramName ) {
case 'manualthumb':
case 'alt':
- // @fixme - possibly check validity here for
+ // @todo Fixme: possibly check validity here for
// manualthumb? downstream behavior seems odd with
// missing manual thumbs.
$validated = true;
@@ -4367,7 +4622,11 @@ class Parser
$params['frame']['caption'] = $caption;
- $params['frame']['title'] = $this->stripAltText( $caption, $holders );
+ # Will the image be presented in a frame, with the caption below?
+ $imageIsFramed = isset( $params['frame']['frame'] ) ||
+ isset( $params['frame']['framed'] ) ||
+ isset( $params['frame']['thumbnail'] ) ||
+ isset( $params['frame']['manualthumb'] );
# In the old days, [[Image:Foo|text...]] would set alt text. Later it
# came to also set the caption, ordinary text after the image -- which
@@ -4385,11 +4644,27 @@ class Parser
# named parameter entirely for images without a caption; adding an ex-
# plicit caption= parameter and preserving the old magic unnamed para-
# meter for BC; ...
- if( $caption !== '' && !isset( $params['frame']['alt'] )
- && !isset( $params['frame']['framed'] )
- && !isset( $params['frame']['thumbnail'] )
- && !isset( $params['frame']['manualthumb'] ) ) {
- $params['frame']['alt'] = $params['frame']['title'];
+ if ( $imageIsFramed ) { # Framed image
+ if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
+ # No caption or alt text, add the filename as the alt text so
+ # that screen readers at least get some description of the image
+ $params['frame']['alt'] = $title->getText();
+ }
+ # Do not set $params['frame']['title'] because tooltips don't make sense
+ # for framed images
+ } else { # Inline image
+ if ( !isset( $params['frame']['alt'] ) ) {
+ # No alt text, use the "caption" for the alt text
+ if ( $caption !== '') {
+ $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
+ } else {
+ # No caption, fall back to using the filename for the
+ # alt text
+ $params['frame']['alt'] = $title->getText();
+ }
+ }
+ # Use the "caption" for the tooltip text
+ $params['frame']['title'] = $this->stripAltText( $caption, $holders );
}
wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
@@ -4404,7 +4679,7 @@ class Parser
return $ret;
}
-
+
protected function stripAltText( $caption, $holders ) {
# Strip bad stuff out of the title (tooltip). We can't just use
# replaceLinkHoldersText() here, because if this function is called
@@ -4420,7 +4695,7 @@ class Parser
# remove the tags
$tooltip = $this->mStripState->unstripBoth( $tooltip );
$tooltip = Sanitizer::stripAllTags( $tooltip );
-
+
return $tooltip;
}
@@ -4452,9 +4727,9 @@ class Parser
/**#@+
* 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 ); }
+ 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 ); }
/**#@-*/
/**#@+
@@ -4856,7 +5131,7 @@ class Parser
$links['interwiki'][] = $this->mLinkHolders->interwiki[$key];
$pos = $start_pos + strlen( "<!--IWLINK $key-->" );
}
-
+
$data['linkholder'] = $links;
return $data;
@@ -4865,7 +5140,7 @@ class Parser
function unserialiseHalfParsedText( $data, $intPrefix = null /* Unique identifying prefix */ ) {
if (!$intPrefix)
$intPrefix = $this->getRandomString();
-
+
// First, extract the strip state.
$stripState = $data['stripstate'];
$this->mStripState->general->merge( $stripState->general );
diff --git a/includes/parser/ParserCache.php b/includes/parser/ParserCache.php
index d17214c3..524d6be5 100644
--- a/includes/parser/ParserCache.php
+++ b/includes/parser/ParserCache.php
@@ -7,7 +7,7 @@ class ParserCache {
/**
* Get an instance of this object
*/
- public static function &singleton() {
+ public static function singleton() {
static $instance;
if ( !isset( $instance ) ) {
global $parserMemc;
@@ -22,11 +22,11 @@ class ParserCache {
*
* @param object $memCached
*/
- function __construct( &$memCached ) {
- $this->mMemc =& $memCached;
+ function __construct( $memCached ) {
+ $this->mMemc = $memCached;
}
- function getKey( &$article, $popts ) {
+ function getKey( $article, $popts ) {
global $wgRequest;
if( $popts instanceof User ) // It used to be getKey( &$article, &$user )
@@ -47,52 +47,55 @@ class ParserCache {
return $key;
}
- function getETag( &$article, $popts ) {
+ function getETag( $article, $popts ) {
return 'W/"' . $this->getKey($article, $popts) . "--" . $article->mTouched. '"';
}
- function get( &$article, $popts ) {
- global $wgCacheEpoch;
- $fname = 'ParserCache::get';
- wfProfileIn( $fname );
-
+ function getDirty( $article, $popts ) {
$key = $this->getKey( $article, $popts );
-
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 {
+ return is_object( $value ) ? $value : false;
+ }
+
+ function get( $article, $popts ) {
+ global $wgCacheEpoch;
+ wfProfileIn( __METHOD__ );
+
+ $value = $this->getDirty( $article, $popts );
+ if ( !$value ) {
wfDebug( "Parser cache miss.\n" );
wfIncrStats( "pcache_miss_absent" );
+ wfProfileOut( __METHOD__ );
+ return false;
+ }
+
+ wfDebug( "Found.\n" );
+ # Invalid 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" );
+ }
$value = false;
+ } else {
+ if ( isset( $value->mTimestamp ) ) {
+ $article->mTimestamp = $value->mTimestamp;
+ }
+ wfIncrStats( "pcache_hit" );
}
- wfProfileOut( $fname );
+ wfProfileOut( __METHOD__ );
return $value;
}
- function save( $parserOutput, &$article, $popts ){
+ function save( $parserOutput, $article, $popts ){
global $wgParserCacheExpireTime;
$key = $this->getKey( $article, $popts );
diff --git a/includes/parser/ParserOptions.php b/includes/parser/ParserOptions.php
index e6a9f3a7..985bba28 100644
--- a/includes/parser/ParserOptions.php
+++ b/includes/parser/ParserOptions.php
@@ -5,10 +5,8 @@
* @todo document
* @ingroup Parser
*/
-class ParserOptions
-{
+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
@@ -35,9 +33,8 @@ class ParserOptions
var $mUser; # Stored user object, just used to initialise the skin
var $mIsPreview; # Parsing the page for a "preview" operation
var $mIsSectionPreview; # Parsing the page for a "preview" operation on a single section
- var $mIsPrintable; # Parsing the printable version of the page
+ var $mIsPrintable; # Parsing the printable version of the page
- function getUseTeX() { return $this->mUseTeX; }
function getUseDynamicDates() { return $this->mUseDynamicDates; }
function getInterwikiMagic() { return $this->mInterwikiMagic; }
function getAllowExternalImages() { return $this->mAllowExternalImages; }
@@ -59,8 +56,8 @@ class ParserOptions
function getExternalLinkTarget() { return $this->mExternalLinkTarget; }
function getIsPreview() { return $this->mIsPreview; }
function getIsSectionPreview() { return $this->mIsSectionPreview; }
- function getIsPrintable() { return $this->mIsPrintable; }
-
+ function getIsPrintable() { return $this->mIsPrintable; }
+
function getSkin() {
if ( !isset( $this->mSkin ) ) {
$this->mSkin = $this->mUser->getSkin();
@@ -82,7 +79,6 @@ class ParserOptions
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 ); }
@@ -107,8 +103,8 @@ class ParserOptions
function setExternalLinkTarget( $x ) { return wfSetVar( $this->mExternalLinkTarget, $x ); }
function setIsPreview( $x ) { return wfSetVar( $this->mIsPreview, $x ); }
function setIsSectionPreview( $x ) { return wfSetVar( $this->mIsSectionPreview, $x ); }
- function setIsPrintable( $x ) { return wfSetVar( $this->mIsPrintable, $x ); }
-
+ function setIsPrintable( $x ) { return wfSetVar( $this->mIsPrintable, $x ); }
+
function __construct( $user = null ) {
$this->initialiseFromUser( $user );
}
@@ -123,12 +119,13 @@ class ParserOptions
/** Get user options */
function initialiseFromUser( $userInput ) {
- global $wgUseTeX, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
+ global $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
global $wgAllowExternalImagesFrom, $wgEnableImageWhitelist, $wgAllowSpecialInclusion, $wgMaxArticleSize;
global $wgMaxPPNodeCount, $wgMaxTemplateDepth, $wgMaxPPExpandDepth, $wgCleanSignatures;
global $wgExternalLinkTarget;
- $fname = 'ParserOptions::initialiseFromUser';
- wfProfileIn( $fname );
+
+ wfProfileIn( __METHOD__ );
+
if ( !$userInput ) {
global $wgUser;
if ( isset( $wgUser ) ) {
@@ -142,7 +139,6 @@ class ParserOptions
$this->mUser = $user;
- $this->mUseTeX = $wgUseTeX;
$this->mUseDynamicDates = $wgUseDynamicDates;
$this->mInterwikiMagic = $wgInterwikiMagic;
$this->mAllowExternalImages = $wgAllowExternalImages;
@@ -167,6 +163,7 @@ class ParserOptions
$this->mExternalLinkTarget = $wgExternalLinkTarget;
$this->mIsPreview = false;
$this->mIsSectionPreview = false;
- wfProfileOut( $fname );
+
+ wfProfileOut( __METHOD__ );
}
}
diff --git a/includes/parser/ParserOutput.php b/includes/parser/ParserOutput.php
index 22c1dfba..ea5840e6 100644
--- a/includes/parser/ParserOutput.php
+++ b/includes/parser/ParserOutput.php
@@ -24,14 +24,10 @@ class ParserOutput
$mOutputHooks = array(), # Hook tags as per $wgParserOutputHooks
$mWarnings = array(), # Warning text to be returned to the user. Wikitext formatted, in the key only
$mSections = array(), # Table of contents
- $mProperties = array(); # Name/value pairs to be cached in the DB
+ $mProperties = array(), # Name/value pairs to be cached in the DB
+ $mTOCHTML = ''; # HTML of the TOC
private $mIndexPolicy = ''; # 'index' or 'noindex'? Any other value will result in no change.
- /**
- * Overridden title for display
- */
- private $displayTitle = false;
-
function ParserOutput( $text = '', $languageLinks = array(), $categoryLinks = array(),
$containsOldMagic = false, $titletext = '' )
{
@@ -54,10 +50,12 @@ class ParserOutput
function &getImages() { return $this->mImages; }
function &getExternalLinks() { return $this->mExternalLinks; }
function getNoGallery() { return $this->mNoGallery; }
+ function getHeadItems() { return $this->mHeadItems; }
function getSubtitle() { return $this->mSubtitle; }
function getOutputHooks() { return (array)$this->mOutputHooks; }
function getWarnings() { return array_keys( $this->mWarnings ); }
function getIndexPolicy() { return $this->mIndexPolicy; }
+ function getTOCHTML() { return $this->mTOCHTML; }
function containsOldMagic() { return $this->mContainsOldMagic; }
function setText( $text ) { return wfSetVar( $this->mText, $text ); }
@@ -68,10 +66,10 @@ class ParserOutput
function setTitleText( $t ) { return wfSetVar( $this->mTitleText, $t ); }
function setSections( $toc ) { return wfSetVar( $this->mSections, $toc ); }
function setIndexPolicy( $policy ) { return wfSetVar( $this->mIndexPolicy, $policy ); }
+ function setTOCHTML( $tochtml ) { return wfSetVar( $this->mTOCHTML, $tochtml ); }
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 ) {
@@ -91,7 +89,18 @@ class ParserOutput
return (bool)$this->mNewSection;
}
+ function addExternalLink( $url ) {
+ # We don't register links pointing to our own server, unless... :-)
+ global $wgServer, $wgRegisterInternalExternals;
+ if( $wgRegisterInternalExternals or stripos($url,$wgServer.'/')!==0)
+ $this->mExternalLinks[$url] = 1;
+ }
+
function addLink( $title, $id = null ) {
+ if ( $title->isExternal() ) {
+ // Don't record interwikis in pagelinks
+ return;
+ }
$ns = $title->getNamespace();
$dbk = $title->getDBkey();
if ( $ns == NS_MEDIA ) {
@@ -170,7 +179,7 @@ class ParserOutput
* @param string $text Desired title text
*/
public function setDisplayTitle( $text ) {
- $this->displayTitle = $text;
+ $this->setTitleText( $text );
}
/**
@@ -179,7 +188,11 @@ class ParserOutput
* @return string
*/
public function getDisplayTitle() {
- return $this->displayTitle;
+ $t = $this->getTitleText( );
+ if( $t === '' ) {
+ return false;
+ }
+ return $t;
}
/**
diff --git a/includes/parser/Preprocessor.php b/includes/parser/Preprocessor.php
index 1a33ac7f..9c417d23 100644
--- a/includes/parser/Preprocessor.php
+++ b/includes/parser/Preprocessor.php
@@ -66,6 +66,21 @@ interface PPFrame {
function isEmpty();
/**
+ * Returns all arguments of this frame
+ */
+ function getArguments();
+
+ /**
+ * Returns all numbered arguments of this frame
+ */
+ function getNumberedArguments();
+
+ /**
+ * Returns all named arguments of this frame
+ */
+ function getNamedArguments();
+
+ /**
* Get an argument to this frame by name
*/
function getArgument( $name );
diff --git a/includes/parser/Preprocessor_DOM.php b/includes/parser/Preprocessor_DOM.php
index 2e114545..673ac241 100644
--- a/includes/parser/Preprocessor_DOM.php
+++ b/includes/parser/Preprocessor_DOM.php
@@ -419,7 +419,8 @@ class Preprocessor_DOM implements Preprocessor {
'count' => $count );
$stack->push( $piece );
$accum =& $stack->getAccum();
- extract( $stack->getFlags() );
+ $flags = $stack->getFlags();
+ extract( $flags );
$i += $count;
}
}
@@ -470,7 +471,8 @@ class Preprocessor_DOM implements Preprocessor {
// Unwind the stack
$stack->pop();
$accum =& $stack->getAccum();
- extract( $stack->getFlags() );
+ $flags = $stack->getFlags();
+ extract( $flags );
// Append the result to the enclosing accumulator
$accum .= $element;
@@ -497,7 +499,8 @@ class Preprocessor_DOM implements Preprocessor {
$stack->push( $piece );
$accum =& $stack->getAccum();
- extract( $stack->getFlags() );
+ $flags = $stack->getFlags();
+ extract( $flags );
} else {
# Add literal brace(s)
$accum .= htmlspecialchars( str_repeat( $curChar, $count ) );
@@ -597,8 +600,8 @@ class Preprocessor_DOM implements Preprocessor {
}
$enclosingAccum .= str_repeat( $piece->open, $skippedBraces );
}
-
- extract( $stack->getFlags() );
+ $flags = $stack->getFlags();
+ extract( $flags );
# Add XML element to the enclosing accumulator
$accum .= $element;
@@ -1189,6 +1192,18 @@ class PPFrame_DOM implements PPFrame {
}
}
+ function getArguments() {
+ return array();
+ }
+
+ function getNumberedArguments() {
+ return array();
+ }
+
+ function getNamedArguments() {
+ return array();
+ }
+
/**
* Returns true if there are no arguments in this frame
*/
@@ -1224,8 +1239,7 @@ class PPTemplateFrame_DOM extends PPFrame_DOM {
var $numberedExpansionCache, $namedExpansionCache;
function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
- $this->preprocessor = $preprocessor;
- $this->parser = $preprocessor->parser;
+ PPFrame_DOM::__construct( $preprocessor );
$this->parent = $parent;
$this->numberedArgs = $numberedArgs;
$this->namedArgs = $namedArgs;
@@ -1337,8 +1351,7 @@ class PPCustomFrame_DOM extends PPFrame_DOM {
var $args;
function __construct( $preprocessor, $args ) {
- $this->preprocessor = $preprocessor;
- $this->parser = $preprocessor->parser;
+ PPFrame_DOM::__construct( $preprocessor );
$this->args = $args;
}
diff --git a/includes/parser/Preprocessor_Hash.php b/includes/parser/Preprocessor_Hash.php
index f46ee40c..c5d69685 100644
--- a/includes/parser/Preprocessor_Hash.php
+++ b/includes/parser/Preprocessor_Hash.php
@@ -1139,6 +1139,18 @@ class PPFrame_Hash implements PPFrame {
}
}
+ function getArguments() {
+ return array();
+ }
+
+ function getNumberedArguments() {
+ return array();
+ }
+
+ function getNamedArguments() {
+ return array();
+ }
+
/**
* Returns true if there are no arguments in this frame
*/
@@ -1174,8 +1186,7 @@ class PPTemplateFrame_Hash extends PPFrame_Hash {
var $numberedExpansionCache, $namedExpansionCache;
function __construct( $preprocessor, $parent = false, $numberedArgs = array(), $namedArgs = array(), $title = false ) {
- $this->preprocessor = $preprocessor;
- $this->parser = $preprocessor->parser;
+ PPFrame_Hash::__construct( $preprocessor );
$this->parent = $parent;
$this->numberedArgs = $numberedArgs;
$this->namedArgs = $namedArgs;
@@ -1287,8 +1298,7 @@ class PPCustomFrame_Hash extends PPFrame_Hash {
var $args;
function __construct( $preprocessor, $args ) {
- $this->preprocessor = $preprocessor;
- $this->parser = $preprocessor->parser;
+ PPFrame_Hash::__construct( $preprocessor );
$this->args = $args;
}
diff --git a/includes/SearchEngine.php b/includes/search/SearchEngine.php
index e5392f7c..f4ca700d 100644
--- a/includes/SearchEngine.php
+++ b/includes/search/SearchEngine.php
@@ -21,11 +21,10 @@ class SearchEngine {
/**
* Perform a full text search query and return a result set.
* If title searches are not supported or disabled, return null.
+ * STUB
*
- * @param string $term - Raw search term
+ * @param $term String: raw search term
* @return SearchResultSet
- * @access public
- * @abstract
*/
function searchText( $term ) {
return null;
@@ -34,11 +33,10 @@ class SearchEngine {
/**
* Perform a title-only search query and return a result set.
* If title searches are not supported or disabled, return null.
+ * STUB
*
- * @param string $term - Raw search term
+ * @param $term String: raw search term
* @return SearchResultSet
- * @access public
- * @abstract
*/
function searchTitle( $term ) {
return null;
@@ -50,6 +48,18 @@ class SearchEngine {
}
/**
+ * When overridden in derived class, performs database-specific conversions
+ * on text to be used for searching or updating search index.
+ * Default implementation does nothing (simply returns $string).
+ *
+ * @param $string string: String to process
+ * @return string
+ */
+ public function normalizeText( $string ) {
+ return $string;
+ }
+
+ /**
* Transform search term in cases when parts of the query came as different GET params (when supported)
* e.g. for prefix queries: search=test&prefix=Main_Page/Archive -> test prefix:Main Page/Archive
*/
@@ -58,27 +68,41 @@ class SearchEngine {
}
/**
- * If an exact title match can be find, or a very slightly close match,
+ * If an exact title match can be found, or a very slightly close match,
* return the title. If no match, returns NULL.
*
- * @param string $term
+ * @param $searchterm String
* @return Title
*/
public static function getNearMatch( $searchterm ) {
+ $title = self::getNearMatchInternal( $searchterm );
+
+ wfRunHooks( 'SearchGetNearMatchComplete', array( $searchterm, &$title ) );
+ return $title;
+ }
+
+ /**
+ * Really find the title match.
+ */
+ private static function getNearMatchInternal( $searchterm ) {
global $wgContLang;
$allSearchTerms = array($searchterm);
- if($wgContLang->hasVariants()){
+ if ( $wgContLang->hasVariants() ) {
$allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm));
}
- foreach($allSearchTerms as $term){
+ if( !wfRunHooks( 'SearchGetNearMatchBefore', array( $allSearchTerms, &$titleResult ) ) ) {
+ return $titleResult;
+ }
+
+ foreach($allSearchTerms as $term) {
# Exact match? No need to look further.
$title = Title::newFromText( $term );
if (is_null($title))
- return NULL;
+ return null;
if ( $title->getNamespace() == NS_SPECIAL || $title->isExternal() || $title->exists() ) {
return $title;
@@ -160,7 +184,7 @@ class SearchEngine {
return SearchEngine::getNearMatch( $matches[1] );
}
- return NULL;
+ return null;
}
public static function legalSearchChars() {
@@ -171,9 +195,8 @@ class SearchEngine {
* Set the maximum number of results to return
* and how many to skip before returning the first.
*
- * @param int $limit
- * @param int $offset
- * @access public
+ * @param $limit Integer
+ * @param $offset Integer
*/
function setLimitOffset( $limit, $offset = 0 ) {
$this->limit = intval( $limit );
@@ -184,8 +207,7 @@ class SearchEngine {
* Set which namespaces the search should include.
* Give an array of namespace index numbers.
*
- * @param array $namespaces
- * @access public
+ * @param $namespaces Array
*/
function setNamespaces( $namespaces ) {
$this->namespaces = $namespaces;
@@ -195,15 +217,17 @@ class SearchEngine {
* Parse some common prefixes: all (search everything)
* or namespace names
*
- * @param string $query
+ * @param $query String
*/
function replacePrefixes( $query ){
global $wgContLang;
- if( strpos($query,':') === false )
- return $query; // nothing to do
-
$parsed = $query;
+ if( strpos($query,':') === false ) { // nothing to do
+ wfRunHooks( 'SearchEngineReplacePrefixesComplete', array( $this, $query, &$parsed ) );
+ return $parsed;
+ }
+
$allkeyword = wfMsgForContent('searchall').":";
if( strncmp($query, $allkeyword, strlen($allkeyword)) == 0 ){
$this->namespaces = null;
@@ -217,14 +241,16 @@ class SearchEngine {
}
}
if(trim($parsed) == '')
- return $query; // prefix was the whole query
+ $parsed = $query; // prefix was the whole query
+
+ wfRunHooks( 'SearchEngineReplacePrefixesComplete', array( $this, $query, &$parsed ) );
return $parsed;
}
/**
* Make a list of searchable namespaces and their canonical names.
- * @return array
+ * @return Array
*/
public static function searchableNamespaces() {
global $wgContLang;
@@ -234,6 +260,8 @@ class SearchEngine {
$arr[$ns] = $name;
}
}
+
+ wfRunHooks( 'SearchableNamespaces', array( &$arr ) );
return $arr;
}
@@ -241,26 +269,35 @@ class SearchEngine {
* Extract default namespaces to search from the given user's
* settings, returning a list of index numbers.
*
- * @param User $user
- * @return array
- * @static
+ * @param $user User
+ * @return Array
*/
- public static function userNamespaces( &$user ) {
- $arr = array();
- foreach( SearchEngine::searchableNamespaces() as $ns => $name ) {
- if( $user->getOption( 'searchNs' . $ns ) ) {
- $arr[] = $ns;
- }
- }
+ public static function userNamespaces( $user ) {
+ global $wgSearchEverythingOnlyLoggedIn;
+
+ // get search everything preference, that can be set to be read for logged-in users
+ $searcheverything = false;
+ if( ( $wgSearchEverythingOnlyLoggedIn && $user->isLoggedIn() )
+ || !$wgSearchEverythingOnlyLoggedIn )
+ $searcheverything = $user->getOption('searcheverything');
+
+ // searcheverything overrides other options
+ if( $searcheverything )
+ return array_keys(SearchEngine::searchableNamespaces());
+
+ $arr = Preferences::loadOldSearchNs( $user );
+ $searchableNamespaces = SearchEngine::searchableNamespaces();
+
+ $arr = array_intersect( $arr, array_keys($searchableNamespaces) ); // Filter
+
return $arr;
}
/**
* Find snippet highlight settings for a given user
*
- * @param User $user
- * @return array contextlines, contextchars
- * @static
+ * @param $user User
+ * @return Array contextlines, contextchars
*/
public static function userHighlightPrefs( &$user ){
//$contextlines = $user->getOption( 'contextlines', 5 );
@@ -273,8 +310,7 @@ class SearchEngine {
/**
* An array of namespaces indexes to be searched by default
*
- * @return array
- * @static
+ * @return Array
*/
public static function defaultNamespaces(){
global $wgNamespacesToBeSearchedDefault;
@@ -286,7 +322,7 @@ class SearchEngine {
* Get a list of namespace names useful for showing in tooltips
* and preferences
*
- * @param unknown_type $namespaces
+ * @param $namespaces Array
*/
public static function namespacesAsText( $namespaces ){
global $wgContLang;
@@ -300,37 +336,21 @@ class SearchEngine {
}
/**
- * An array of "project" namespaces indexes typically searched
- * by logged-in users
+ * Return the help namespaces to be shown on Special:Search
*
- * @return array
- * @static
+ * @return Array
*/
- public static function projectNamespaces() {
- global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject;
+ public static function helpNamespaces() {
+ global $wgNamespacesToBeSearchedHelp;
- return array_keys( $wgNamespacesToBeSearchedProject, true );
- }
-
- /**
- * An array of "project" namespaces indexes typically searched
- * by logged-in users in addition to the default namespaces
- *
- * @return array
- * @static
- */
- public static function defaultAndProjectNamespaces() {
- global $wgNamespacesToBeSearchedDefault, $wgNamespacesToBeSearchedProject;
-
- return array_keys( $wgNamespacesToBeSearchedDefault +
- $wgNamespacesToBeSearchedProject, true);
+ return array_keys( $wgNamespacesToBeSearchedHelp, true );
}
/**
* Return a 'cleaned up' search string
*
- * @return string
- * @access public
+ * @param $text String
+ * @return String
*/
function filter( $text ) {
$lc = $this->legalSearchChars();
@@ -358,11 +378,11 @@ class SearchEngine {
/**
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
+ * STUB
*
- * @param int $id
- * @param string $title
- * @param string $text
- * @abstract
+ * @param $id Integer
+ * @param $title String
+ * @param $text String
*/
function update( $id, $title, $text ) {
// no-op
@@ -371,10 +391,10 @@ class SearchEngine {
/**
* Update a search index record's title only.
* Title should be pre-processed.
+ * STUB
*
- * @param int $id
- * @param string $title
- * @abstract
+ * @param $id Integer
+ * @param $title String
*/
function updateTitle( $id, $title ) {
// no-op
@@ -383,8 +403,7 @@ class SearchEngine {
/**
* Get OpenSearch suggestion template
*
- * @return string
- * @static
+ * @return String
*/
public static function getOpenSearchTemplate() {
global $wgOpenSearchTemplate, $wgServer, $wgScriptPath;
@@ -400,8 +419,7 @@ class SearchEngine {
/**
* Get internal MediaWiki Suggest template
*
- * @return string
- * @static
+ * @return String
*/
public static function getMWSuggestTemplate() {
global $wgMWSuggestTemplate, $wgServer, $wgScriptPath;
@@ -419,10 +437,9 @@ class SearchResultSet {
/**
* Fetch an array of regular expression fragments for matching
* the search terms as parsed by this engine in a text extract.
+ * STUB
*
- * @return array
- * @access public
- * @abstract
+ * @return Array
*/
function termMatches() {
return array();
@@ -434,8 +451,9 @@ class SearchResultSet {
/**
* Return true if results are included in this result set.
- * @return bool
- * @abstract
+ * STUB
+ *
+ * @return Boolean
*/
function hasResults() {
return false;
@@ -449,8 +467,7 @@ class SearchResultSet {
*
* Return null if no total hits number is supported.
*
- * @return int
- * @access public
+ * @return Integer
*/
function getTotalHits() {
return null;
@@ -460,22 +477,21 @@ class SearchResultSet {
* Some search modes return a suggested alternate term if there are
* no exact hits. Returns true if there is one on this set.
*
- * @return bool
- * @access public
+ * @return Boolean
*/
function hasSuggestion() {
return false;
}
/**
- * @return string suggested query, null if none
+ * @return String: suggested query, null if none
*/
function getSuggestionQuery(){
return null;
}
/**
- * @return string HTML highlighted suggested query, '' if none
+ * @return String: HTML highlighted suggested query, '' if none
*/
function getSuggestionSnippet(){
return '';
@@ -485,7 +501,7 @@ class SearchResultSet {
* Return information about how and from where the results were fetched,
* should be useful for diagnostics and debugging
*
- * @return string
+ * @return String
*/
function getInfo() {
return null;
@@ -503,7 +519,7 @@ class SearchResultSet {
/**
* Check if there are results on other wikis
*
- * @return boolean
+ * @return Boolean
*/
function hasInterwikiResults() {
return $this->getInterwikiResults() != null;
@@ -512,9 +528,9 @@ class SearchResultSet {
/**
* Fetches next search result, or false.
+ * STUB
+ *
* @return SearchResult
- * @access public
- * @abstract
*/
function next() {
return false;
@@ -522,13 +538,43 @@ class SearchResultSet {
/**
* Frees the result set, if applicable.
- * @ access public
*/
function free() {
// ...
}
}
+/**
+ * This class is used for different SQL-based search engines shipped with MediaWiki
+ */
+class SqlSearchResultSet extends SearchResultSet {
+ function __construct( $resultSet, $terms ) {
+ $this->mResultSet = $resultSet;
+ $this->mTerms = $terms;
+ }
+
+ function termMatches() {
+ return $this->mTerms;
+ }
+
+ function numRows() {
+ return $this->mResultSet->numRows();
+ }
+
+ function next() {
+ if ($this->mResultSet === false )
+ return false;
+
+ $row = $this->mResultSet->fetchObject();
+ if ($row === false)
+ return false;
+ return new SearchResult($row);
+ }
+
+ function free() {
+ $this->mResultSet->free();
+ }
+}
/**
* @ingroup Search
@@ -539,9 +585,9 @@ class SearchResultTooMany {
/**
- * @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.
+ * @todo 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 {
@@ -560,8 +606,7 @@ class SearchResult {
/**
* Check if this is result points to an invalid title
*
- * @return boolean
- * @access public
+ * @return Boolean
*/
function isBrokenTitle(){
if( is_null($this->mTitle) )
@@ -572,8 +617,7 @@ class SearchResult {
/**
* Check if target page is missing, happens when index is out of date
*
- * @return boolean
- * @access public
+ * @return Boolean
*/
function isMissingRevision(){
return !$this->mRevision && !$this->mImage;
@@ -581,14 +625,13 @@ class SearchResult {
/**
* @return Title
- * @access public
*/
function getTitle() {
return $this->mTitle;
}
/**
- * @return double or null if not supported
+ * @return Double or null if not supported
*/
function getScore() {
return null;
@@ -608,8 +651,8 @@ class SearchResult {
}
/**
- * @param array $terms terms to highlight
- * @return string highlighted text snippet, null (and not '') if not supported
+ * @param $terms Array: terms to highlight
+ * @return String: highlighted text snippet, null (and not '') if not supported
*/
function getTextSnippet($terms){
global $wgUser, $wgAdvancedSearchHighlighting;
@@ -623,16 +666,16 @@ class SearchResult {
}
/**
- * @param array $terms terms to highlight
- * @return string highlighted title, '' if not supported
+ * @param $terms Array: 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
+ * @param $terms Array: terms to highlight
+ * @return String: highlighted redirect name (redirect to this page), '' if none or not supported
*/
function getRedirectSnippet($terms){
return '';
@@ -660,7 +703,7 @@ class SearchResult {
}
/**
- * @return string timestamp
+ * @return String: timestamp
*/
function getTimestamp(){
if( $this->mRevision )
@@ -671,7 +714,7 @@ class SearchResult {
}
/**
- * @return int number of words
+ * @return Integer: number of words
*/
function getWordCount(){
$this->initText();
@@ -679,7 +722,7 @@ class SearchResult {
}
/**
- * @return int size in bytes
+ * @return Integer: size in bytes
*/
function getByteSize(){
$this->initText();
@@ -687,14 +730,14 @@ class SearchResult {
}
/**
- * @return boolean if hit has related articles
+ * @return Boolean if hit has related articles
*/
function hasRelated(){
return false;
}
/**
- * @return interwiki prefix of the title (return iw even if title is broken)
+ * @return String: interwiki prefix of the title (return iw even if title is broken)
*/
function getInterwikiPrefix(){
return '';
@@ -716,11 +759,11 @@ class SearchHighlighter {
/**
* Default implementation of wikitext highlighting
*
- * @param string $text
- * @param array $terms Terms to highlight (unescaped)
- * @param int $contextlines
- * @param int $contextchars
- * @return string
+ * @param $text String
+ * @param $terms Array: terms to highlight (unescaped)
+ * @param $contextlines Integer
+ * @param $contextchars Integer
+ * @return String
*/
public function highlightText( $text, $terms, $contextlines, $contextchars ) {
global $wgLang, $wgContLang;
@@ -958,9 +1001,9 @@ class SearchHighlighter {
/**
* Split text into lines and add it to extracts array
*
- * @param array $extracts index -> $line
- * @param int $count
- * @param string $text
+ * @param $extracts Array: index -> $line
+ * @param $count Integer
+ * @param $text String
*/
function splitAndAdd(&$extracts, &$count, $text){
$split = explode( "\n", $this->mCleanWikitext? $this->removeWiki($text) : $text );
@@ -974,7 +1017,7 @@ class SearchHighlighter {
/**
* Do manual case conversion for non-ascii chars
*
- * @param unknown_type $matches
+ * @param $matches Array
*/
function caseCallback($matches){
global $wgContLang;
@@ -987,12 +1030,12 @@ class SearchHighlighter {
/**
* 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
+ * @param $text String
+ * @param $start Integer
+ * @param $end Integer
+ * @param $posStart Integer: (out) actual start position
+ * @param $posEnd Integer: (out) actual end position
+ * @return String
*/
function extract($text, $start, $end, &$posStart = null, &$posEnd = null ){
global $wgContLang;
@@ -1018,10 +1061,10 @@ class SearchHighlighter {
/**
* 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
+ * @param $text String
+ * @param $point Integer
+ * @param $offset Integer: offset to found index
+ * @return Integer: nearest nonletter index, or beginning of utf8 char if none
*/
function position($text, $point, $offset=0 ){
$tolerance = 10;
@@ -1048,12 +1091,12 @@ class SearchHighlighter {
/**
* 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
+ * @param $pattern String: regexp for matching lines
+ * @param $extracts Array: extracts to search
+ * @param $linesleft Integer: number of extracts to make
+ * @param $contextchars Integer: length of snippet
+ * @param $out Array: map for highlighted snippets
+ * @param $offsets Array: map of starting points of snippets
* @protected
*/
function process( $pattern, $extracts, &$linesleft, &$contextchars, &$out, &$offsets ){
@@ -1120,7 +1163,7 @@ class SearchHighlighter {
* callback to replace [[target|caption]] kind of links, if
* the target is category or image, leave it
*
- * @param array $matches
+ * @param $matches Array
*/
function linkReplace($matches){
$colon = strpos( $matches[1], ':' );
@@ -1140,11 +1183,11 @@ class SearchHighlighter {
* Simple & fast snippet extraction, but gives completely unrelevant
* snippets
*
- * @param string $text
- * @param array $terms
- * @param int $contextlines
- * @param int $contextchars
- * @return string
+ * @param $text String
+ * @param $terms Array
+ * @param $contextlines Integer
+ * @param $contextchars Integer
+ * @return String
*/
public function highlightSimple( $text, $terms, $contextlines, $contextchars ) {
global $wgLang, $wgContLang;
@@ -1196,7 +1239,7 @@ class SearchHighlighter {
/**
* Dummy class to be used when non-supported Database engine is present.
- * @fixme Dummy class should probably try something at least mildly useful,
+ * @todo Fixme: dummy class should probably try something at least mildly useful,
* such as a LIKE search through titles.
* @ingroup Search
*/
diff --git a/includes/SearchIBM_DB2.php b/includes/search/SearchIBM_DB2.php
index 57813a73..d7587186 100644
--- a/includes/SearchIBM_DB2.php
+++ b/includes/search/SearchIBM_DB2.php
@@ -34,32 +34,29 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param string $term - Raw search term
- * @return IBM_DB2SearchResultSet
- * @access public
+ * @param $term String: raw search term
+ * @return SqlSearchResultSet
*/
function searchText( $term ) {
$resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), true)));
- return new IBM_DB2SearchResultSet($resultSet, $this->searchTerms);
+ return new SqlSearchResultSet($resultSet, $this->searchTerms);
}
/**
* Perform a title-only search query and return a result set.
*
- * @param string $term - Raw search term
- * @return IBM_DB2SearchResultSet
- * @access public
+ * @param $term String: taw search term
+ * @return SqlSearchResultSet
*/
function searchTitle($term) {
$resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), false)));
- return new MySQLSearchResultSet($resultSet, $this->searchTerms);
+ return new SqlSearchResultSet($resultSet, $this->searchTerms);
}
/**
* Return a partial WHERE clause to exclude redirects, if so set
- * @return string
- * @private
+ * @return String
*/
function queryRedirect() {
if ($this->showRedirects) {
@@ -71,8 +68,7 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Return a partial WHERE clause to limit the search to the given namespaces
- * @return string
- * @private
+ * @return String
*/
function queryNamespaces() {
if( is_null($this->namespaces) )
@@ -86,8 +82,7 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Return a LIMIT clause to limit results on the query.
- * @return string
- * @private
+ * @return String
*/
function queryLimit($sql) {
return $this->db->limitResult($sql, $this->limit, $this->offset);
@@ -96,8 +91,7 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Does not do anything for generic search engine
* subclasses may define this though
- * @return string
- * @private
+ * @return String
*/
function queryRanking($filteredTerm, $fulltext) {
// requires Net Search Extender or equivalent
@@ -108,9 +102,8 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Construct the full SQL query to do the search.
* The guts shoulds be constructed in queryMain()
- * @param string $filteredTerm
- * @param bool $fulltext
- * @private
+ * @param string $filteredTerm String
+ * @param bool $fulltext Boolean
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' .
@@ -122,8 +115,8 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Picks which field to index on, depending on what type of query.
- * @param bool $fulltext
- * @return string
+ * @param $fulltext Boolean
+ * @return String
*/
function getIndexField($fulltext) {
return $fulltext ? 'si_text' : 'si_title';
@@ -132,10 +125,9 @@ class SearchIBM_DB2 extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param string $filteredTerm
- * @param bool $fulltext
- * @return string
- * @private
+ * @param string $filteredTerm String
+ * @param bool $fulltext Boolean
+ * @return String
*/
function queryMain( $filteredTerm, $fulltext ) {
$match = $this->parseQuery($filteredTerm, $fulltext);
@@ -159,7 +151,17 @@ class SearchIBM_DB2 extends SearchEngine {
if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
$filteredText, $m, PREG_SET_ORDER)) {
foreach($m as $terms) {
- $q[] = $terms[1] . $wgContLang->stripForSearch($terms[2]);
+
+ // Search terms in all variant forms, only
+ // apply on wiki with LanguageConverter
+ $temp_terms = $wgContLang->autoConvertToAllVariants( $terms[2] );
+ if( is_array( $temp_terms )) {
+ $temp_terms = array_unique( array_values( $temp_terms ));
+ foreach( $temp_terms as $t )
+ $q[] = $terms[1] . $wgContLang->normalizeForSearch( $t );
+ }
+ else
+ $q[] = $terms[1] . $wgContLang->normalizeForSearch( $terms[2] );
if (!empty($terms[3])) {
$regexp = preg_quote( $terms[3], '/' );
@@ -185,9 +187,9 @@ class SearchIBM_DB2 extends SearchEngine {
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
*
- * @param int $id
- * @param string $title
- * @param string $text
+ * @param $id Integer
+ * @param $title String
+ * @param $text String
*/
function update($id, $title, $text) {
$dbw = wfGetDB(DB_MASTER);
@@ -207,8 +209,8 @@ class SearchIBM_DB2 extends SearchEngine {
* Update a search index record's title only.
* Title should be pre-processed.
*
- * @param int $id
- * @param string $title
+ * @param $id Integer
+ * @param $title String
*/
function updateTitle($id, $title) {
$dbw = wfGetDB(DB_MASTER);
@@ -220,28 +222,3 @@ class SearchIBM_DB2 extends SearchEngine {
array());
}
}
-
-/**
- * @ingroup Search
- */
-class IBM_DB2SearchResultSet extends SearchResultSet {
- function __construct($resultSet, $terms) {
- $this->mResultSet = $resultSet;
- $this->mTerms = $terms;
- }
-
- function termMatches() {
- return $this->mTerms;
- }
-
- function numRows() {
- return $this->mResultSet->numRows();
- }
-
- function next() {
- $row = $this->mResultSet->fetchObject();
- if ($row === false)
- return false;
- return new SearchResult($row);
- }
-}
diff --git a/includes/search/SearchMySQL.php b/includes/search/SearchMySQL.php
new file mode 100644
index 00000000..0c238be8
--- /dev/null
+++ b/includes/search/SearchMySQL.php
@@ -0,0 +1,412 @@
+<?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
+
+/**
+ * @file
+ * @ingroup Search
+ */
+
+/**
+ * Search engine hook for MySQL 4+
+ * @ingroup Search
+ */
+class SearchMySQL extends SearchEngine {
+ var $strictMatching = true;
+ static $mMinSearchLength;
+
+ /** @todo document */
+ function __construct( $db ) {
+ $this->db = $db;
+ }
+
+ /**
+ * Parse the user's query and transform it into an SQL fragment which will
+ * become part of a WHERE clause
+ */
+ function parseQuery( $filteredText, $fulltext ) {
+ global $wgContLang;
+ $lc = SearchEngine::legalSearchChars(); // Minus format chars
+ $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 $bits ) {
+ @list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
+
+ if( $nonQuoted != '' ) {
+ $term = $nonQuoted;
+ $quote = '';
+ } else {
+ $term = str_replace( '"', '', $term );
+ $quote = '"';
+ }
+
+ if( $searchon !== '' ) $searchon .= ' ';
+ if( $this->strictMatching && ($modifier == '') ) {
+ // If we leave this out, boolean op defaults to OR which is rarely helpful.
+ $modifier = '+';
+ }
+
+ // Some languages such as Serbian store the input form in the search index,
+ // so we may need to search for matches in multiple writing system variants.
+ $convertedVariants = $wgContLang->autoConvertToAllVariants( $term );
+ if( is_array( $convertedVariants ) ) {
+ $variants = array_unique( array_values( $convertedVariants ) );
+ } else {
+ $variants = array( $term );
+ }
+
+ // The low-level search index does some processing on input to work
+ // around problems with minimum lengths and encoding in MySQL's
+ // fulltext engine.
+ // For Chinese this also inserts spaces between adjacent Han characters.
+ $strippedVariants = array_map(
+ array( $wgContLang, 'normalizeForSearch' ),
+ $variants );
+
+ // Some languages such as Chinese force all variants to a canonical
+ // form when stripping to the low-level search index, so to be sure
+ // let's check our variants list for unique items after stripping.
+ $strippedVariants = array_unique( $strippedVariants );
+
+ $searchon .= $modifier;
+ if( count( $strippedVariants) > 1 )
+ $searchon .= '(';
+ foreach( $strippedVariants as $stripped ) {
+ $stripped = $this->normalizeText( $stripped );
+ if( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
+ // Hack for Chinese: we need to toss in quotes for
+ // multiple-character phrases since normalizeForSearch()
+ // added spaces between them to make word breaks.
+ $stripped = '"' . trim( $stripped ) . '"';
+ }
+ $searchon .= "$quote$stripped$quote$wildcard ";
+ }
+ if( count( $strippedVariants) > 1 )
+ $searchon .= ')';
+
+ // Match individual terms or quoted phrase in result highlighting...
+ // Note that variants will be introduced in a later stage for highlighting!
+ $regexp = $this->regexTerm( $term, $wildcard );
+ $this->searchTerms[] = $regexp;
+ }
+ wfDebug( __METHOD__ . ": Would search with '$searchon'\n" );
+ wfDebug( __METHOD__ . ': Match with /' . implode( '|', $this->searchTerms ) . "/\n" );
+ } else {
+ wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" );
+ }
+
+ $searchon = $this->db->strencode( $searchon );
+ $field = $this->getIndexField( $fulltext );
+ return " MATCH($field) AGAINST('$searchon' IN BOOLEAN MODE) ";
+ }
+
+ function regexTerm( $string, $wildcard ) {
+ global $wgContLang;
+
+ $regex = preg_quote( $string, '/' );
+ if( $wgContLang->hasWordBreaks() ) {
+ if( $wildcard ) {
+ // Don't cut off the final bit!
+ $regex = "\b$regex";
+ } else {
+ $regex = "\b$regex\b";
+ }
+ } else {
+ // For Chinese, words may legitimately abut other words in the text literal.
+ // Don't add \b boundary checks... note this could cause false positives
+ // for latin chars.
+ }
+ return $regex;
+ }
+
+ public static function legalSearchChars() {
+ return "\"*" . parent::legalSearchChars();
+ }
+
+ /**
+ * Perform a full text search query and return a result set.
+ *
+ * @param $term String: raw search term
+ * @return MySQLSearchResultSet
+ */
+ function searchText( $term ) {
+ return $this->searchInternal( $term, true );
+ }
+
+ /**
+ * Perform a title-only search query and return a result set.
+ *
+ * @param $term String: raw search term
+ * @return MySQLSearchResultSet
+ */
+ function searchTitle( $term ) {
+ return $this->searchInternal( $term, false );
+ }
+
+ protected function searchInternal( $term, $fulltext ) {
+ global $wgCountTotalSearchHits;
+
+ $filteredTerm = $this->filter( $term );
+ $resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) );
+
+ $total = null;
+ if( $wgCountTotalSearchHits ) {
+ $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
+ $row = $totalResult->fetchObject();
+ if( $row ) {
+ $total = intval( $row->c );
+ }
+ $totalResult->free();
+ }
+
+ return new MySQLSearchResultSet( $resultSet, $this->searchTerms, $total );
+ }
+
+
+ /**
+ * Return a partial WHERE clause to exclude redirects, if so set
+ * @return String
+ */
+ function queryRedirect() {
+ if( $this->showRedirects ) {
+ return '';
+ } else {
+ return 'AND page_is_redirect=0';
+ }
+ }
+
+ /**
+ * Return a partial WHERE clause to limit the search to the given namespaces
+ * @return String
+ */
+ function queryNamespaces() {
+ if( is_null($this->namespaces) )
+ return ''; # search all
+ if ( !count( $this->namespaces ) ) {
+ $namespaces = '0';
+ } else {
+ $namespaces = $this->db->makeList( $this->namespaces );
+ }
+ return 'AND page_namespace IN (' . $namespaces . ')';
+ }
+
+ /**
+ * Return a LIMIT clause to limit results on the query.
+ * @return String
+ */
+ function queryLimit() {
+ return $this->db->limitResult( '', $this->limit, $this->offset );
+ }
+
+ /**
+ * Does not do anything for generic search engine
+ * subclasses may define this though
+ * @return String
+ */
+ function queryRanking( $filteredTerm, $fulltext ) {
+ return '';
+ }
+
+ /**
+ * Construct the full SQL query to do the search.
+ * The guts shoulds be constructed in queryMain()
+ * @param $filteredTerm String
+ * @param $fulltext Boolean
+ */
+ function getQuery( $filteredTerm, $fulltext ) {
+ return $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
+ $this->queryRedirect() . ' ' .
+ $this->queryNamespaces() . ' ' .
+ $this->queryRanking( $filteredTerm, $fulltext ) . ' ' .
+ $this->queryLimit();
+ }
+
+ /**
+ * Picks which field to index on, depending on what type of query.
+ * @param $fulltext Boolean
+ * @return String
+ */
+ function getIndexField( $fulltext ) {
+ return $fulltext ? 'si_text' : 'si_title';
+ }
+
+ /**
+ * Get the base part of the search query.
+ * The actual match syntax will depend on the server
+ * version; MySQL 3 and MySQL 4 have different capabilities
+ * in their fulltext search indexes.
+ *
+ * @param $filteredTerm String
+ * @param $fulltext Boolean
+ * @return String
+ */
+ function queryMain( $filteredTerm, $fulltext ) {
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+ $page = $this->db->tableName( 'page' );
+ $searchindex = $this->db->tableName( 'searchindex' );
+ return 'SELECT page_id, page_namespace, page_title ' .
+ "FROM $page,$searchindex " .
+ 'WHERE page_id=si_page AND ' . $match;
+ }
+
+ function getCountQuery( $filteredTerm, $fulltext ) {
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+ $page = $this->db->tableName( 'page' );
+ $searchindex = $this->db->tableName( 'searchindex' );
+ return "SELECT COUNT(*) AS c " .
+ "FROM $page,$searchindex " .
+ 'WHERE page_id=si_page AND ' . $match .
+ $this->queryRedirect() . ' ' .
+ $this->queryNamespaces();
+ }
+
+ /**
+ * Create or update the search index record for the given page.
+ * Title and text should be pre-processed.
+ *
+ * @param $id Integer
+ * @param $title String
+ * @param $text String
+ */
+ function update( $id, $title, $text ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->replace( 'searchindex',
+ array( 'si_page' ),
+ array(
+ 'si_page' => $id,
+ 'si_title' => $this->normalizeText( $title ),
+ 'si_text' => $this->normalizeText( $text )
+ ), __METHOD__ );
+ }
+
+ /**
+ * Update a search index record's title only.
+ * Title should be pre-processed.
+ *
+ * @param $id Integer
+ * @param $title String
+ */
+ function updateTitle( $id, $title ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'searchindex',
+ array( 'si_title' => $this->normalizeText( $title ) ),
+ array( 'si_page' => $id ),
+ __METHOD__,
+ array( $dbw->lowPriorityOption() ) );
+ }
+
+ /**
+ * Converts some characters for MySQL's indexing to grok it correctly,
+ * and pads short words to overcome limitations.
+ */
+ function normalizeText( $string ) {
+ global $wgContLang;
+
+ wfProfileIn( __METHOD__ );
+
+ // Some languages such as Chinese require word segmentation
+ $out = $wgContLang->wordSegmentation( $string );
+
+ // MySQL fulltext index doesn't grok utf-8, so we
+ // need to fold cases and convert to hex
+ $out = preg_replace_callback(
+ "/([\\xc0-\\xff][\\x80-\\xbf]*)/",
+ array( $this, 'stripForSearchCallback' ),
+ $wgContLang->lc( $out ) );
+
+ // And to add insult to injury, the default indexing
+ // ignores short words... Pad them so we can pass them
+ // through without reconfiguring the server...
+ $minLength = $this->minSearchLength();
+ if( $minLength > 1 ) {
+ $n = $minLength - 1;
+ $out = preg_replace(
+ "/\b(\w{1,$n})\b/",
+ "$1u800",
+ $out );
+ }
+
+ // Periods within things like hostnames and IP addresses
+ // are also important -- we want a search for "example.com"
+ // or "192.168.1.1" to work sanely.
+ //
+ // MySQL's search seems to ignore them, so you'd match on
+ // "example.wikipedia.com" and "192.168.83.1" as well.
+ $out = preg_replace(
+ "/(\w)\.(\w|\*)/u",
+ "$1u82e$2",
+ $out );
+
+ wfProfileOut( __METHOD__ );
+
+ return $out;
+ }
+
+ /**
+ * Armor a case-folded UTF-8 string to get through MySQL's
+ * fulltext search without being mucked up by funny charset
+ * settings or anything else of the sort.
+ */
+ protected function stripForSearchCallback( $matches ) {
+ return 'u8' . bin2hex( $matches[1] );
+ }
+
+ /**
+ * Check MySQL server's ft_min_word_len setting so we know
+ * if we need to pad short words...
+ *
+ * @return int
+ */
+ protected function minSearchLength() {
+ if( is_null( self::$mMinSearchLength ) ) {
+ $sql = "SHOW GLOBAL VARIABLES LIKE 'ft\\_min\\_word\\_len'";
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $result = $dbr->query( $sql );
+ $row = $result->fetchObject();
+ $result->free();
+
+ if( $row && $row->Variable_name == 'ft_min_word_len' ) {
+ self::$mMinSearchLength = intval( $row->Value );
+ } else {
+ self::$mMinSearchLength = 0;
+ }
+ }
+ return self::$mMinSearchLength;
+ }
+}
+
+/**
+ * @ingroup Search
+ */
+class MySQLSearchResultSet extends SqlSearchResultSet {
+ function MySQLSearchResultSet( $resultSet, $terms, $totalHits=null ) {
+ parent::__construct( $resultSet, $terms );
+ $this->mTotalHits = $totalHits;
+ }
+
+ function getTotalHits() {
+ return $this->mTotalHits;
+ }
+} \ No newline at end of file
diff --git a/includes/SearchMySQL4.php b/includes/search/SearchMySQL4.php
index 3e2bb2d1..3e2bb2d1 100644
--- a/includes/SearchMySQL4.php
+++ b/includes/search/SearchMySQL4.php
diff --git a/includes/SearchOracle.php b/includes/search/SearchOracle.php
index b48d5e6e..e4c5deee 100644
--- a/includes/SearchOracle.php
+++ b/includes/search/SearchOracle.php
@@ -27,6 +27,34 @@
* @ingroup Search
*/
class SearchOracle extends SearchEngine {
+
+ private $reservedWords = array ('ABOUT' => 1,
+ 'ACCUM' => 1,
+ 'AND' => 1,
+ 'BT' => 1,
+ 'BTG' => 1,
+ 'BTI' => 1,
+ 'BTP' => 1,
+ 'FUZZY' => 1,
+ 'HASPATH' => 1,
+ 'INPATH' => 1,
+ 'MINUS' => 1,
+ 'NEAR' => 1,
+ 'NOT' => 1,
+ 'NT' => 1,
+ 'NTG' => 1,
+ 'NTI' => 1,
+ 'NTP' => 1,
+ 'OR' => 1,
+ 'PT' => 1,
+ 'RT' => 1,
+ 'SQE' => 1,
+ 'SYN' => 1,
+ 'TR' => 1,
+ 'TRSYN' => 1,
+ 'TT' => 1,
+ 'WITHIN' => 1);
+
function __construct($db) {
$this->db = $db;
}
@@ -34,23 +62,27 @@ class SearchOracle extends SearchEngine {
/**
* Perform a full text search query and return a result set.
*
- * @param string $term - Raw search term
- * @return OracleSearchResultSet
- * @access public
+ * @param $term String: raw search term
+ * @return SqlSearchResultSet
*/
function searchText( $term ) {
+ if ($term == '')
+ return new SqlSearchResultSet(false, '');
+
$resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), true)));
- return new OracleSearchResultSet($resultSet, $this->searchTerms);
+ return new SqlSearchResultSet($resultSet, $this->searchTerms);
}
/**
* Perform a title-only search query and return a result set.
*
- * @param string $term - Raw search term
- * @return ORacleSearchResultSet
- * @access public
+ * @param $term String: raw search term
+ * @return SqlSearchResultSet
*/
function searchTitle($term) {
+ if ($term == '')
+ return new SqlSearchResultSet(false, '');
+
$resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), false)));
return new MySQLSearchResultSet($resultSet, $this->searchTerms);
}
@@ -58,8 +90,7 @@ class SearchOracle extends SearchEngine {
/**
* Return a partial WHERE clause to exclude redirects, if so set
- * @return string
- * @private
+ * @return String
*/
function queryRedirect() {
if ($this->showRedirects) {
@@ -71,8 +102,7 @@ class SearchOracle extends SearchEngine {
/**
* Return a partial WHERE clause to limit the search to the given namespaces
- * @return string
- * @private
+ * @return String
*/
function queryNamespaces() {
if( is_null($this->namespaces) )
@@ -87,8 +117,7 @@ class SearchOracle extends SearchEngine {
/**
* Return a LIMIT clause to limit results on the query.
- * @return string
- * @private
+ * @return String
*/
function queryLimit($sql) {
return $this->db->limitResult($sql, $this->limit, $this->offset);
@@ -97,8 +126,7 @@ class SearchOracle extends SearchEngine {
/**
* Does not do anything for generic search engine
* subclasses may define this though
- * @return string
- * @private
+ * @return String
*/
function queryRanking($filteredTerm, $fulltext) {
return ' ORDER BY score(1)';
@@ -107,9 +135,8 @@ class SearchOracle extends SearchEngine {
/**
* Construct the full SQL query to do the search.
* The guts shoulds be constructed in queryMain()
- * @param string $filteredTerm
- * @param bool $fulltext
- * @private
+ * @param $filteredTerm String
+ * @param $fulltext Boolean
*/
function getQuery( $filteredTerm, $fulltext ) {
return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' .
@@ -121,8 +148,8 @@ class SearchOracle extends SearchEngine {
/**
* Picks which field to index on, depending on what type of query.
- * @param bool $fulltext
- * @return string
+ * @param $fulltext Boolean
+ * @return String
*/
function getIndexField($fulltext) {
return $fulltext ? 'si_text' : 'si_title';
@@ -131,10 +158,9 @@ class SearchOracle extends SearchEngine {
/**
* Get the base part of the search query.
*
- * @param string $filteredTerm
- * @param bool $fulltext
- * @return string
- * @private
+ * @param $filteredTerm String
+ * @param $fulltext Boolean
+ * @return String
*/
function queryMain( $filteredTerm, $fulltext ) {
$match = $this->parseQuery($filteredTerm, $fulltext);
@@ -145,8 +171,8 @@ class SearchOracle extends SearchEngine {
'WHERE page_id=si_page AND ' . $match;
}
- /**
- * Parse a user input search string, and return an SQL fragment to be used
+ /**
+ * Parse a user input search string, and return an SQL fragment to be used
* as part of a WHERE clause
*/
function parseQuery($filteredText, $fulltext) {
@@ -156,13 +182,22 @@ class SearchOracle extends SearchEngine {
# FIXME: This doesn't handle parenthetical expressions.
$m = array();
- $q = array();
-
+ $searchon = '';
if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
$filteredText, $m, PREG_SET_ORDER)) {
foreach($m as $terms) {
- $q[] = $terms[1] . $wgContLang->stripForSearch($terms[2]);
-
+ // Search terms in all variant forms, only
+ // apply on wiki with LanguageConverter
+ $temp_terms = $wgContLang->autoConvertToAllVariants( $terms[2] );
+ if( is_array( $temp_terms )) {
+ $temp_terms = array_unique( array_values( $temp_terms ));
+ foreach( $temp_terms as $t ) {
+ $searchon .= ($terms[1] == '-' ? ' ~' : ' & ') . $this->escapeTerm( $t );
+ }
+ }
+ else {
+ $searchon .= ($terms[1] == '-' ? ' ~' : ' & ') . $this->escapeTerm( $terms[2] );
+ }
if (!empty($terms[3])) {
$regexp = preg_quote( $terms[3], '/' );
if ($terms[4])
@@ -174,18 +209,27 @@ class SearchOracle extends SearchEngine {
}
}
- $searchon = $this->db->addQuotes(join(',', $q));
+
+ $searchon = $this->db->addQuotes(ltrim($searchon, ' &'));
$field = $this->getIndexField($fulltext);
return " CONTAINS($field, $searchon, 1) > 0 ";
}
+ private function escapeTerm($t) {
+ global $wgContLang;
+ $t = $wgContLang->normalizeForSearch($t);
+ $t = isset($this->reservedWords[strtoupper($t)]) ? '{'.$t.'}' : $t;
+ $t = preg_replace('/^"(.*)"$/', '($1)', $t);
+ $t = preg_replace('/([-&|])/', '\\\\$1', $t);
+ return $t;
+ }
/**
* Create or update the search index record for the given page.
* Title and text should be pre-processed.
*
- * @param int $id
- * @param string $title
- * @param string $text
+ * @param $id Integer
+ * @param $title String
+ * @param $text String
*/
function update($id, $title, $text) {
$dbw = wfGetDB(DB_MASTER);
@@ -216,29 +260,9 @@ class SearchOracle extends SearchEngine {
'SearchOracle::updateTitle',
array());
}
-}
-
-/**
- * @ingroup Search
- */
-class OracleSearchResultSet extends SearchResultSet {
- function __construct($resultSet, $terms) {
- $this->mResultSet = $resultSet;
- $this->mTerms = $terms;
- }
- function termMatches() {
- return $this->mTerms;
- }
-
- function numRows() {
- return $this->mResultSet->numRows();
- }
- function next() {
- $row = $this->mResultSet->fetchObject();
- if ($row === false)
- return false;
- return new SearchResult($row);
+ public static function legalSearchChars() {
+ return "\"" . parent::legalSearchChars();
}
}
diff --git a/includes/SearchPostgres.php b/includes/search/SearchPostgres.php
index fa9d8420..0006fa82 100644
--- a/includes/SearchPostgres.php
+++ b/includes/search/SearchPostgres.php
@@ -37,9 +37,8 @@ class SearchPostgres extends SearchEngine {
* Currently searches a page's current title (page.page_title) and
* latest revision article text (pagecontent.old_text)
*
- * @param string $term - Raw search term
+ * @param $term String: raw search term
* @return PostgresSearchResultSet
- * @access public
*/
function searchTitle( $term ) {
$q = $this->searchQuery( $term , 'titlevector', 'page_title' );
@@ -130,9 +129,8 @@ class SearchPostgres extends SearchEngine {
/**
* Construct the full SQL query to do the search.
- * @param string $filteredTerm
- * @param string $fulltext
- * @private
+ * @param $filteredTerm String
+ * @param $fulltext String
*/
function searchQuery( $term, $fulltext, $colname ) {
global $wgDBversion;
@@ -232,18 +230,9 @@ class PostgresSearchResult extends SearchResult {
/**
* @ingroup Search
*/
-class PostgresSearchResultSet extends SearchResultSet {
+class PostgresSearchResultSet extends SqlSearchResultSet {
function __construct( $resultSet, $terms ) {
- $this->mResultSet = $resultSet;
- $this->mTerms = $terms;
- }
-
- function termMatches() {
- return $this->mTerms;
- }
-
- function numRows() {
- return $this->mResultSet->numRows();
+ parent::__construct( $resultSet, $terms );
}
function next() {
diff --git a/includes/search/SearchSqlite.php b/includes/search/SearchSqlite.php
new file mode 100644
index 00000000..fb55efec
--- /dev/null
+++ b/includes/search/SearchSqlite.php
@@ -0,0 +1,344 @@
+<?php
+# SQLite search backend, based upon SearchMysql
+#
+# 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 Search
+ */
+
+/**
+ * Search engine hook for SQLite
+ * @ingroup Search
+ */
+class SearchSqlite extends SearchEngine {
+ // Cached because SearchUpdate keeps recreating our class
+ private static $fulltextSupported = null;
+
+ /**
+ * Creates an instance of this class
+ * @param $db DatabaseSqlite: database object
+ */
+ function __construct( $db ) {
+ $this->db = $db;
+ }
+
+ /**
+ * Whether fulltext search is supported by current schema
+ * @return Boolean
+ */
+ function fulltextSearchSupported() {
+ if ( self::$fulltextSupported === null ) {
+ self::$fulltextSupported = $this->db->selectField(
+ 'updatelog',
+ 'ul_key',
+ array( 'ul_key' => 'fts3' ),
+ __METHOD__ ) !== false;
+ }
+ return self::$fulltextSupported;
+ }
+
+ /**
+ * Parse the user's query and transform it into an SQL fragment which will
+ * become part of a WHERE clause
+ */
+ function parseQuery( $filteredText, $fulltext ) {
+ global $wgContLang;
+ $lc = SearchEngine::legalSearchChars(); // Minus format chars
+ $searchon = '';
+ $this->searchTerms = array();
+
+ $m = array();
+ if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
+ $filteredText, $m, PREG_SET_ORDER ) ) {
+ foreach( $m as $bits ) {
+ @list( /* all */, $modifier, $term, $nonQuoted, $wildcard ) = $bits;
+
+ if( $nonQuoted != '' ) {
+ $term = $nonQuoted;
+ $quote = '';
+ } else {
+ $term = str_replace( '"', '', $term );
+ $quote = '"';
+ }
+
+ if( $searchon !== '' ) $searchon .= ' ';
+
+ // Some languages such as Serbian store the input form in the search index,
+ // so we may need to search for matches in multiple writing system variants.
+ $convertedVariants = $wgContLang->autoConvertToAllVariants( $term );
+ if( is_array( $convertedVariants ) ) {
+ $variants = array_unique( array_values( $convertedVariants ) );
+ } else {
+ $variants = array( $term );
+ }
+
+ // The low-level search index does some processing on input to work
+ // around problems with minimum lengths and encoding in MySQL's
+ // fulltext engine.
+ // For Chinese this also inserts spaces between adjacent Han characters.
+ $strippedVariants = array_map(
+ array( $wgContLang, 'normalizeForSearch' ),
+ $variants );
+
+ // Some languages such as Chinese force all variants to a canonical
+ // form when stripping to the low-level search index, so to be sure
+ // let's check our variants list for unique items after stripping.
+ $strippedVariants = array_unique( $strippedVariants );
+
+ $searchon .= $modifier;
+ if( count( $strippedVariants) > 1 )
+ $searchon .= '(';
+ foreach( $strippedVariants as $stripped ) {
+ if( $nonQuoted && strpos( $stripped, ' ' ) !== false ) {
+ // Hack for Chinese: we need to toss in quotes for
+ // multiple-character phrases since normalizeForSearch()
+ // added spaces between them to make word breaks.
+ $stripped = '"' . trim( $stripped ) . '"';
+ }
+ $searchon .= "$quote$stripped$quote$wildcard ";
+ }
+ if( count( $strippedVariants) > 1 )
+ $searchon .= ')';
+
+ // Match individual terms or quoted phrase in result highlighting...
+ // Note that variants will be introduced in a later stage for highlighting!
+ $regexp = $this->regexTerm( $term, $wildcard );
+ $this->searchTerms[] = $regexp;
+ }
+
+ } else {
+ wfDebug( __METHOD__ . ": Can't understand search query '{$filteredText}'\n" );
+ }
+
+ $searchon = $this->db->strencode( $searchon );
+ $field = $this->getIndexField( $fulltext );
+ return " $field MATCH '$searchon' ";
+ }
+
+ function regexTerm( $string, $wildcard ) {
+ global $wgContLang;
+
+ $regex = preg_quote( $string, '/' );
+ if( $wgContLang->hasWordBreaks() ) {
+ if( $wildcard ) {
+ // Don't cut off the final bit!
+ $regex = "\b$regex";
+ } else {
+ $regex = "\b$regex\b";
+ }
+ } else {
+ // For Chinese, words may legitimately abut other words in the text literal.
+ // Don't add \b boundary checks... note this could cause false positives
+ // for latin chars.
+ }
+ return $regex;
+ }
+
+ public static function legalSearchChars() {
+ return "\"*" . parent::legalSearchChars();
+ }
+
+ /**
+ * Perform a full text search query and return a result set.
+ *
+ * @param $term String: raw search term
+ * @return SqliteSearchResultSet
+ */
+ function searchText( $term ) {
+ return $this->searchInternal( $term, true );
+ }
+
+ /**
+ * Perform a title-only search query and return a result set.
+ *
+ * @param $term String: raw search term
+ * @return SqliteSearchResultSet
+ */
+ function searchTitle( $term ) {
+ return $this->searchInternal( $term, false );
+ }
+
+ protected function searchInternal( $term, $fulltext ) {
+ global $wgCountTotalSearchHits, $wgContLang;
+
+ if ( !$this->fulltextSearchSupported() ) {
+ return null;
+ }
+
+ $filteredTerm = $this->filter( $wgContLang->lc( $term ) );
+ $resultSet = $this->db->query( $this->getQuery( $filteredTerm, $fulltext ) );
+
+ $total = null;
+ if( $wgCountTotalSearchHits ) {
+ $totalResult = $this->db->query( $this->getCountQuery( $filteredTerm, $fulltext ) );
+ $row = $totalResult->fetchObject();
+ if( $row ) {
+ $total = intval( $row->c );
+ }
+ $totalResult->free();
+ }
+
+ return new SqliteSearchResultSet( $resultSet, $this->searchTerms, $total );
+ }
+
+
+ /**
+ * Return a partial WHERE clause to exclude redirects, if so set
+ * @return String
+ */
+ function queryRedirect() {
+ if( $this->showRedirects ) {
+ return '';
+ } else {
+ return 'AND page_is_redirect=0';
+ }
+ }
+
+ /**
+ * Return a partial WHERE clause to limit the search to the given namespaces
+ * @return String
+ */
+ function queryNamespaces() {
+ if( is_null($this->namespaces) )
+ return ''; # search all
+ if ( !count( $this->namespaces ) ) {
+ $namespaces = '0';
+ } else {
+ $namespaces = $this->db->makeList( $this->namespaces );
+ }
+ return 'AND page_namespace IN (' . $namespaces . ')';
+ }
+
+ /**
+ * Returns a query with limit for number of results set.
+ * @param $sql String:
+ * @return String
+ */
+ function limitResult( $sql ) {
+ return $this->db->limitResult( $sql, $this->limit, $this->offset );
+ }
+
+ /**
+ * Construct the full SQL query to do the search.
+ * The guts shoulds be constructed in queryMain()
+ * @param $filteredTerm String
+ * @param $fulltext Boolean
+ */
+ function getQuery( $filteredTerm, $fulltext ) {
+ return $this->limitResult(
+ $this->queryMain( $filteredTerm, $fulltext ) . ' ' .
+ $this->queryRedirect() . ' ' .
+ $this->queryNamespaces()
+ );
+ }
+
+ /**
+ * Picks which field to index on, depending on what type of query.
+ * @param $fulltext Boolean
+ * @return String
+ */
+ function getIndexField( $fulltext ) {
+ return $fulltext ? 'si_text' : 'si_title';
+ }
+
+ /**
+ * Get the base part of the search query.
+ *
+ * @param $filteredTerm String
+ * @param $fulltext Boolean
+ * @return String
+ */
+ function queryMain( $filteredTerm, $fulltext ) {
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+ $page = $this->db->tableName( 'page' );
+ $searchindex = $this->db->tableName( 'searchindex' );
+ return "SELECT $searchindex.rowid, page_namespace, page_title " .
+ "FROM $page,$searchindex " .
+ "WHERE page_id=$searchindex.rowid AND $match";
+ }
+
+ function getCountQuery( $filteredTerm, $fulltext ) {
+ $match = $this->parseQuery( $filteredTerm, $fulltext );
+ $page = $this->db->tableName( 'page' );
+ $searchindex = $this->db->tableName( 'searchindex' );
+ return "SELECT COUNT(*) AS c " .
+ "FROM $page,$searchindex " .
+ "WHERE page_id=$searchindex.rowid AND $match" .
+ $this->queryRedirect() . ' ' .
+ $this->queryNamespaces();
+ }
+
+ /**
+ * Create or update the search index record for the given page.
+ * Title and text should be pre-processed.
+ *
+ * @param $id Integer
+ * @param $title String
+ * @param $text String
+ */
+ function update( $id, $title, $text ) {
+ if ( !$this->fulltextSearchSupported() ) {
+ return;
+ }
+ // @todo: find a method to do it in a single request,
+ // couldn't do it so far due to typelessness of FTS3 tables.
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->delete( 'searchindex', array( 'rowid' => $id ), __METHOD__ );
+
+ $dbw->insert( 'searchindex',
+ array(
+ 'rowid' => $id,
+ 'si_title' => $title,
+ 'si_text' => $text
+ ), __METHOD__ );
+ }
+
+ /**
+ * Update a search index record's title only.
+ * Title should be pre-processed.
+ *
+ * @param $id Integer
+ * @param $title String
+ */
+ function updateTitle( $id, $title ) {
+ if ( !$this->fulltextSearchSupported() ) {
+ return;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'searchindex',
+ array( 'si_title' => $title ),
+ array( 'rowid' => $id ),
+ __METHOD__ );
+ }
+}
+
+/**
+ * @ingroup Search
+ */
+class SqliteSearchResultSet extends SqlSearchResultSet {
+ function SqliteSearchResultSet( $resultSet, $terms, $totalHits=null ) {
+ parent::__construct( $resultSet, $terms );
+ $this->mTotalHits = $totalHits;
+ }
+
+ function getTotalHits() {
+ return $this->mTotalHits;
+ }
+} \ No newline at end of file
diff --git a/includes/SearchUpdate.php b/includes/search/SearchUpdate.php
index 087a8ba5..e30c70e6 100644
--- a/includes/SearchUpdate.php
+++ b/includes/search/SearchUpdate.php
@@ -43,11 +43,11 @@ class SearchUpdate {
}
# Language-specific strip/conversion
- $text = $wgContLang->stripForSearch( $this->mText );
+ $text = $wgContLang->normalizeForSearch( $this->mText );
wfProfileIn( $fname.'-regexps' );
- $text = preg_replace( "/<\\/?\\s*[A-Za-z][A-Za-z0-9]*\\s*([^>]*?)>/",
- ' ', strtolower( " " . $text /*$this->mText*/ . " " ) ); # Strip HTML markup
+ $text = preg_replace( "/<\\/?\\s*[A-Za-z][^>]*?>/",
+ ' ', $wgContLang->lc( " " . $text . " " ) ); # Strip HTML markup
$text = preg_replace( "/(^|\\n)==\\s*([^\\n]+)\\s*==(\\s)/sD",
"\\1\\2 \\2 \\2\\3", $text ); # Emphasize headings
diff --git a/includes/specials/SpecialActiveusers.php b/includes/specials/SpecialActiveusers.php
new file mode 100644
index 00000000..7d907fb5
--- /dev/null
+++ b/includes/specials/SpecialActiveusers.php
@@ -0,0 +1,195 @@
+<?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
+
+/**
+ * This class is used to get a list of active users. The ones with specials
+ * rights (sysop, bureaucrat, developer) will have them displayed
+ * next to their names.
+ *
+ * @file
+ * @ingroup SpecialPage
+ */
+class ActiveUsersPager extends UsersPager {
+
+ function __construct( $group = null ) {
+ global $wgRequest, $wgRCMaxAge;
+ $this->RCMaxAge = ceil( $wgRCMaxAge / ( 3600 * 24 ) ); // Constant
+
+ $un = $wgRequest->getText( 'username' );
+ $this->requestedUser = '';
+ if ( $un != '' ) {
+ $username = Title::makeTitleSafe( NS_USER, $un );
+ if( !is_null( $username ) ) {
+ $this->requestedUser = $username->getText();
+ }
+ }
+
+ $this->setupOptions();
+
+ parent::__construct();
+ }
+
+ public function setupOptions() {
+ global $wgRequest;
+
+ $this->opts = new FormOptions();
+
+ $this->opts->add( 'hidebots', false, FormOptions::BOOL );
+ $this->opts->add( 'hidesysops', false, FormOptions::BOOL );
+
+ $this->opts->fetchValuesFromRequest( $wgRequest );
+
+ $this->groups = array();
+ if ($this->opts->getValue('hidebots') == 1)
+ $this->groups['bot'] = true;
+ if ($this->opts->getValue('hidesysops') == 1)
+ $this->groups['sysop'] = true;
+ }
+
+ function getIndexField() {
+ return 'rc_user_text';
+ }
+
+ function getQueryInfo() {
+ $dbr = wfGetDB( DB_SLAVE );
+ $conds = array( 'rc_user > 0' ); // Users - no anons
+ $conds[] = 'ipb_deleted IS NULL'; // don't show hidden names
+ $conds[] = "rc_log_type IS NULL OR rc_log_type != 'newusers'";
+
+ if( $this->requestedUser != '' ) {
+ $conds[] = 'rc_user_text >= ' . $dbr->addQuotes( $this->requestedUser );
+ }
+
+ $query = array(
+ 'tables' => array( 'recentchanges', 'user', 'ipblocks' ),
+ 'fields' => array( 'rc_user_text AS user_name', // inheritance
+ 'rc_user_text', // for Pager
+ 'user_id',
+ 'COUNT(*) AS recentedits',
+ 'MAX(ipb_user) AS blocked'
+ ),
+ 'options' => array(
+ 'GROUP BY' => 'rc_user_text, user_id',
+ 'USE INDEX' => array( 'recentchanges' => 'rc_user_text' )
+ ),
+ 'join_conds' => array(
+ 'user' => array( 'INNER JOIN', 'rc_user_text=user_name' ),
+ 'ipblocks' => array( 'LEFT JOIN', 'user_id=ipb_user AND ipb_auto=0 AND ipb_deleted=1' ),
+ ),
+ 'conds' => $conds
+ );
+ return $query;
+ }
+
+ function formatRow( $row ) {
+ global $wgLang;
+ $userName = $row->user_name;
+
+ $ulinks = $this->getSkin()->userLink( $row->user_id, $userName );
+ $ulinks .= $this->getSkin()->userToolLinks( $row->user_id, $userName );
+
+ $list = array();
+ foreach( self::getGroups( $row->user_id ) as $group ) {
+ if (isset($this->groups[$group]))
+ return;
+ $list[] = self::buildGroupLink( $group );
+ }
+ $groups = $wgLang->commaList( $list );
+
+ $item = wfSpecialList( $ulinks, $groups );
+ $count = wfMsgExt( 'activeusers-count',
+ array( 'parsemag' ),
+ $wgLang->formatNum( $row->recentedits ),
+ $userName,
+ $wgLang->formatNum ( $this->RCMaxAge )
+ );
+ $blocked = $row->blocked ? ' ' . wfMsgExt( 'listusers-blocked', array( 'parsemag' ), $userName ) : '';
+
+ return Html::rawElement( 'li', array(), "{$item} [{$count}]{$blocked}" );
+ }
+
+ function getPageHeader() {
+ global $wgScript, $wgRequest;
+
+ $self = $this->getTitle();
+ $limit = $this->mLimit ? Xml::hidden( 'limit', $this->mLimit ) : '';
+
+ $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ); # Form tag
+ $out .= Xml::fieldset( wfMsg( 'activeusers' ) ) . "\n";
+ $out .= Xml::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n";
+
+ $out .= Xml::inputLabel( wfMsg( 'activeusers-from' ), 'username', 'offset', 20, $this->requestedUser ) . '<br />';# Username field
+
+ $out .= Xml::checkLabel( wfMsg('activeusers-hidebots'), 'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ) );
+
+ $out .= Xml::checkLabel( wfMsg('activeusers-hidesysops'), 'hidesysops', 'hidesysops', $this->opts->getValue( 'hidesysops' ) ) . '<br />';
+
+ $out .= Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n";# Submit button and form bottom
+ $out .= Xml::closeElement( 'fieldset' );
+ $out .= Xml::closeElement( 'form' );
+
+ return $out;
+ }
+}
+
+/**
+ * @ingroup SpecialPage
+ */
+class SpecialActiveUsers extends SpecialPage {
+
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Activeusers' );
+ }
+
+ /**
+ * Show the special page
+ *
+ * @param $par Mixed: parameter passed to the page or null
+ */
+ public function execute( $par ) {
+ global $wgOut, $wgLang, $wgRCMaxAge;
+
+ $this->setHeaders();
+
+ $up = new ActiveUsersPager();
+
+ # getBody() first to check, if empty
+ $usersbody = $up->getBody();
+
+ $s = Html::rawElement( 'div', array( 'class' => 'mw-activeusers-intro' ),
+ wfMsgExt( 'activeusers-intro', array( 'parsemag', 'escape' ), $wgLang->formatNum( ceil( $wgRCMaxAge / 86400 ) ) )
+ );
+
+ $s .= $up->getPageHeader();
+ if( $usersbody ) {
+ $s .= $up->getNavigationBar();
+ $s .= Html::rawElement( 'ul', array(), $usersbody );
+ $s .= $up->getNavigationBar();
+ } else {
+ $s .= Html::element( 'p', array(), wfMsg( 'activeusers-noresult' ) );
+ }
+
+ $wgOut->addHTML( $s );
+ }
+
+}
diff --git a/includes/specials/SpecialAllmessages.php b/includes/specials/SpecialAllmessages.php
index 38181c08..1745bf6c 100644
--- a/includes/specials/SpecialAllmessages.php
+++ b/includes/specials/SpecialAllmessages.php
@@ -4,233 +4,414 @@
* @file
* @ingroup SpecialPage
*/
+class SpecialAllmessages extends SpecialPage {
-/**
- * Constructor.
- */
-function wfSpecialAllmessages() {
- global $wgOut, $wgRequest, $wgMessageCache, $wgTitle;
- global $wgUseDatabaseMessages, $wgLang;
-
- # The page isn't much use if the MediaWiki namespace is not being used
- if( !$wgUseDatabaseMessages ) {
- $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
- return;
+ /**
+ * Constructor
+ */
+ public function __construct() {
+ parent::__construct( 'Allmessages' );
}
- wfProfileIn( __METHOD__ );
+ /**
+ * Show the special page
+ *
+ * @param $par Mixed: parameter passed to the page or null
+ */
+ public function execute( $par ) {
+ global $wgOut, $wgRequest;
- wfProfileIn( __METHOD__ . '-setup' );
- $ot = $wgRequest->getText( 'ot' );
+ $this->setHeaders();
- $navText = wfMsg( 'allmessagestext' );
+ global $wgUseDatabaseMessages;
+ if( !$wgUseDatabaseMessages ) {
+ $wgOut->addWikiMsg( 'allmessagesnotsupportedDB' );
+ return;
+ } else {
+ $this->outputHeader( 'allmessagestext' );
+ }
- # Make sure all extension messages are available
+ $this->filter = $wgRequest->getVal( 'filter', 'all' );
+ $this->prefix = $wgRequest->getVal( 'prefix', '' );
- $wgMessageCache->loadAllMessages();
+ $this->table = new AllmessagesTablePager(
+ $this,
+ $conds = array(),
+ wfGetLangObj( $wgRequest->getVal( 'lang', $par ) )
+ );
- $sortedArray = array_merge( Language::getMessagesFor( 'en' ),
- $wgMessageCache->getExtensionMessagesFor( 'en' ) );
- ksort( $sortedArray );
+ $this->langCode = $this->table->lang->getCode();
+
+ $wgOut->addHTML( $this->buildForm() .
+ $this->table->getNavigationBar() .
+ $this->table->getLimitForm() .
+ $this->table->getBody() .
+ $this->table->getNavigationBar() );
- $messages = array();
- foreach( $sortedArray as $key => $value ) {
- $messages[$key]['enmsg'] = $value;
- $messages[$key]['statmsg'] = wfMsgReal( $key, array(), false, false, false );
- $messages[$key]['msg'] = wfMsgNoTrans( $key );
- $sortedArray[$key] = NULL; // trade bytes from $sortedArray to this
-
}
- unset($sortedArray); // trade bytes from $sortedArray to this
-
- wfProfileOut( __METHOD__ . '-setup' );
-
- wfProfileIn( __METHOD__ . '-output' );
- $wgOut->addScriptFile( 'allmessages.js' );
- if ( $ot == 'php' ) {
- $navText .= wfAllMessagesMakePhp( $messages );
- $wgOut->addHTML( $wgLang->pipeList( array(
- '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( $wgLang->pipeList( array(
- '<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";
- $messages[$key] = NULL; // trade bytes
- }
- $txt .= "</messages>";
- return $txt;
-}
+ function buildForm() {
+ global $wgScript;
-/**
- * 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 = '';
+ $languages = Language::getLanguageNames( false );
+ ksort( $languages );
+
+ $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-allmessages-form' ) ) .
+ Xml::fieldset( wfMsg( 'allmessages-filter-legend' ) ) .
+ Xml::hidden( 'title', $this->getTitle() ) .
+ Xml::openElement( 'table', array( 'class' => 'mw-allmessages-table' ) ) . "\n" .
+ '<tr>
+ <td class="mw-label">' .
+ Xml::label( wfMsg( 'allmessages-prefix' ), 'mw-allmessages-form-prefix' ) .
+ "</td>\n
+ <td class=\"mw-input\">" .
+ Xml::input( 'prefix', 20, str_replace( '_', ' ', $this->prefix ), array( 'id' => 'mw-allmessages-form-prefix' ) ) .
+ "</td>\n
+ </tr>
+ <tr>\n
+ <td class='mw-label'>" .
+ wfMsg( 'allmessages-filter' ) .
+ "</td>\n
+ <td class='mw-input'>" .
+ Xml::radioLabel( wfMsg( 'allmessages-filter-unmodified' ),
+ 'filter',
+ 'unmodified',
+ 'mw-allmessages-form-filter-unmodified',
+ ( $this->filter == 'unmodified' ? true : false )
+ ) .
+ Xml::radioLabel( wfMsg( 'allmessages-filter-all' ),
+ 'filter',
+ 'all',
+ 'mw-allmessages-form-filter-all',
+ ( $this->filter == 'all' ? true : false )
+ ) .
+ Xml::radioLabel( wfMsg( 'allmessages-filter-modified' ),
+ 'filter',
+ 'modified',
+ 'mw-allmessages-form-filter-modified',
+ ( $this->filter == 'modified' ? true : false )
+ ) .
+ "</td>\n
+ </tr>
+ <tr>\n
+ <td class=\"mw-label\">" .
+ Xml::label( wfMsg( 'allmessages-language' ), 'mw-allmessages-form-lang' ) .
+ "</td>\n
+ <td class=\"mw-input\">" .
+ Xml::openElement( 'select', array( 'id' => 'mw-allmessages-form-lang', 'name' => 'lang' ) );
+
+ foreach( $languages as $lang => $name ) {
+ $selected = $lang == $this->langCode ? true : false;
+ $out .= Xml::option( $lang . ' - ' . $name, $lang, $selected ) . "\n";
}
- $txt .= "'$key' => '" . preg_replace( '/(?<!\\\\)\'/', "\'", $m['msg']) . "',$comment\n";
- $messages[$key] = NULL; // trade bytes
+ $out .= Xml::closeElement( 'select' ) .
+ "</td>\n
+ </tr>
+ <tr>\n
+ <td></td>
+ <td>" .
+ Xml::submitButton( wfMsg( 'allmessages-filter-submit' ) ) .
+ "</td>\n
+ </tr>" .
+ Xml::closeElement( 'table' ) .
+ $this->table->getHiddenFields( array( 'title', 'prefix', 'filter', 'lang' ) ) .
+ Xml::closeElement( 'fieldset' ) .
+ Xml::closeElement( 'form' );
+ return $out;
}
- $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.
+/* use TablePager for prettified output. We have to pretend that we're
+ * getting data from a table when in fact not all of it comes from the database.
*/
-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 );
- $res = $dbr->select( 'page',
- array( 'page_namespace', 'page_title' ),
- array( 'page_namespace' => array(NS_MEDIAWIKI,NS_MEDIAWIKI_TALK) ),
- __METHOD__,
- array( 'USE INDEX' => 'name_title' )
- );
- while( $s = $dbr->fetchObject( $res ) ) {
- $pageExists[$s->page_namespace][$s->page_title] = 1;
- }
- $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();
- }
+class AllmessagesTablePager extends TablePager {
- $titleObj = Title::makeTitle( NS_MEDIAWIKI, $title );
- $talkPage = Title::makeTitle( NS_MEDIAWIKI_TALK, $title );
+ public $mLimitsShown;
- $changed = ( $m['statmsg'] != $m['msg'] );
- $message = htmlspecialchars( $m['statmsg'] );
- $mw = htmlspecialchars( $m['msg'] );
+ function __construct( $page, $conds, $langObj = null ) {
+ parent::__construct();
+ $this->mIndexField = 'am_title';
+ $this->mPage = $page;
+ $this->mConds = $conds;
+ $this->mDefaultDirection = true; // always sort ascending
+ // We want to have an option for people to view *all* the messages,
+ // so they can use Ctrl+F to search them. 5000 is the maximum that
+ // will get through WebRequest::getLimitOffset().
+ $this->mLimitsShown = array( 20, 50, 100, 250, 500, 5000 => wfMsg('limitall') );
- if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI] ) ) {
- $pageLink = $sk->makeKnownLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .
- htmlspecialchars( $key ) . '</span>' );
+ global $wgLang, $wgContLang, $wgRequest;
+
+ $this->talk = htmlspecialchars( wfMsg( 'talkpagelinktext' ) );
+
+ $this->lang = ( $langObj ? $langObj : $wgContLang );
+ $this->langcode = $this->lang->getCode();
+ $this->foreign = $this->langcode != $wgContLang->getCode();
+
+ if( $wgRequest->getVal( 'filter', 'all' ) === 'all' ){
+ $this->custom = null; // So won't match in either case
} else {
- $pageLink = $sk->makeBrokenLinkObj( $titleObj, "<span id=\"sp-allmessages-i-$i\">" .
- htmlspecialchars( $key ) . '</span>' );
+ $this->custom = ($wgRequest->getVal( 'filter' ) == 'unmodified');
}
- if( array_key_exists( $title, $pageExists[NS_MEDIAWIKI_TALK] ) ) {
- $talkLink = $sk->makeKnownLinkObj( $talkPage, htmlspecialchars( $talk ) );
+
+ $prefix = $wgLang->ucfirst( $wgRequest->getVal( 'prefix', '' ) );
+ $prefix = $prefix != '' ? Title::makeTitleSafe( NS_MEDIAWIKI, $wgRequest->getVal( 'prefix', null ) ) : null;
+ if( $prefix !== null ){
+ $this->prefix = '/^' . preg_quote( $prefix->getDBkey() ) . '/i';
} else {
- $talkLink = $sk->makeBrokenLinkObj( $talkPage, htmlspecialchars( $talk ) );
+ $this->prefix = false;
}
+ $this->getSkin();
- $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>";
+ // The suffix that may be needed for message names if we're in a
+ // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr'
+ if( $this->foreign ) {
+ $this->suffix = '/' . $this->langcode;
} else {
- $txt .= "
- <tr class=\"def\" id=\"sp-allmessages-r1-$i\">
- <td>
- $anchor$pageLink<br />$talkLink
- </td><td>
- $mw
- </td>
- </tr>";
+ $this->suffix = '';
}
- $messages[$key] = NULL; // trade bytes
- $i++;
}
- $txt .= '</table>';
- wfProfileOut( __METHOD__ . '-output' );
- wfProfileOut( __METHOD__ );
- return $txt;
+ function getAllMessages( $descending ) {
+ wfProfileIn( __METHOD__ );
+ $messageNames = Language::getLocalisationCache()->getSubitemList( 'en', 'messages' );
+ if( $descending ){
+ rsort( $messageNames );
+ } else {
+ asort( $messageNames );
+ }
+
+ // Normalise message names so they look like page titles
+ $messageNames = array_map( array( $this->lang, 'ucfirst' ), $messageNames );
+ wfProfileIn( __METHOD__ );
+
+ return $messageNames;
+ }
+
+ /**
+ * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist.
+ * Returns array( 'pages' => ..., 'talks' => ... ), where the subarrays have
+ * an entry for each existing page, with the key being the message name and
+ * value arbitrary.
+ */
+ function getCustomisedStatuses( $messageNames ) {
+ wfProfileIn( __METHOD__ . '-db' );
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'page',
+ array( 'page_namespace', 'page_title' ),
+ array( 'page_namespace' => array( NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ) ),
+ __METHOD__,
+ array( 'USE INDEX' => 'name_title' )
+ );
+ $xNames = array_flip( $messageNames );
+
+ $pageFlags = $talkFlags = array();
+
+ while( $s = $dbr->fetchObject( $res ) ) {
+ if( $s->page_namespace == NS_MEDIAWIKI ) {
+ if( $this->foreign ) {
+ $title = explode( '/', $s->page_title );
+ if( count( $title ) === 2 && $this->langcode == $title[1]
+ && isset( $xNames[$title[0]] ) )
+ {
+ $pageFlags["{$title[0]}"] = true;
+ }
+ } elseif( isset( $xNames[$s->page_title] ) ) {
+ $pageFlags[$s->page_title] = true;
+ }
+ } else if( $s->page_namespace == NS_MEDIAWIKI_TALK ){
+ $talkFlags[$s->page_title] = true;
+ }
+ }
+ $dbr->freeResult( $res );
+
+ wfProfileOut( __METHOD__ . '-db' );
+
+ return array( 'pages' => $pageFlags, 'talks' => $talkFlags );
+ }
+
+ /* This function normally does a database query to get the results; we need
+ * to make a pretend result using a FakeResultWrapper.
+ */
+ function reallyDoQuery( $offset, $limit, $descending ) {
+ $result = new FakeResultWrapper( array() );
+
+ $messageNames = $this->getAllMessages( $descending );
+ $statuses = $this->getCustomisedStatuses( $messageNames );
+
+ $count = 0;
+ foreach( $messageNames as $key ) {
+ $customised = isset( $statuses['pages'][$key] );
+ if( $customised !== $this->custom &&
+ ( $descending && ( $key < $offset || !$offset ) || !$descending && $key > $offset ) &&
+ ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false )
+ ){
+ $result->result[] = array(
+ 'am_title' => $key,
+ 'am_actual' => wfMsgGetKey( $key, /*useDB*/true, $this->langcode, false ),
+ 'am_default' => wfMsgGetKey( $key, /*useDB*/false, $this->langcode, false ),
+ 'am_customised' => $customised,
+ 'am_talk_exists' => isset( $statuses['talks'][$key] )
+ );
+ $count++;
+ }
+ if( $count == $limit ) break;
+ }
+ return $result;
+ }
+
+ function getStartBody() {
+ return Xml::openElement( 'table', array( 'class' => 'TablePager', 'id' => 'mw-allmessagestable' ) ) . "\n" .
+ "<thead><tr>
+ <th rowspan=\"2\">" .
+ wfMsg( 'allmessagesname' ) . "
+ </th>
+ <th>" .
+ wfMsg( 'allmessagesdefault' ) .
+ "</th>
+ </tr>\n
+ <tr>
+ <th>" .
+ wfMsg( 'allmessagescurrent' ) .
+ "</th>
+ </tr></thead><tbody>\n";
+ }
+
+ function formatValue( $field, $value ){
+ global $wgLang;
+ switch( $field ){
+
+ case 'am_title' :
+
+ $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix );
+ $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix );
+
+ if( $this->mCurrentRow->am_customised ){
+ $title = $this->mSkin->linkKnown( $title, $wgLang->lcfirst( $value ) );
+ } else {
+ $title = $this->mSkin->link(
+ $title,
+ $wgLang->lcfirst( $value ),
+ array(),
+ array(),
+ array( 'broken' )
+ );
+ }
+ if ( $this->mCurrentRow->am_talk_exists ) {
+ $talk = $this->mSkin->linkKnown( $talk , $this->talk );
+ } else {
+ $talk = $this->mSkin->link(
+ $talk,
+ $this->talk,
+ array(),
+ array(),
+ array( 'broken' )
+ );
+ }
+ return $title . ' (' . $talk . ')';
+
+ case 'am_default' :
+ return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES );
+ case 'am_actual' :
+ return Sanitizer::escapeHtmlAllowEntities( $value, ENT_QUOTES );
+ }
+ return '';
+ }
+
+ function formatRow( $row ){
+ // Do all the normal stuff
+ $s = parent::formatRow( $row );
+
+ // But if there's a customised message, add that too.
+ if( $row->am_customised ){
+ $s .= Xml::openElement( 'tr', $this->getRowAttrs( $row, true ) );
+ $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) );
+ if ( $formatted == '' ) {
+ $formatted = '&nbsp;';
+ }
+ $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted )
+ . "</tr>\n";
+ }
+ return $s;
+ }
+
+ function getRowAttrs( $row, $isSecond = false ){
+ $arr = array();
+ global $wgLang;
+ if( $row->am_customised ){
+ $arr['class'] = 'allmessages-customised';
+ }
+ if( !$isSecond ){
+ $arr['id'] = Sanitizer::escapeId( 'msg_' . $wgLang->lcfirst( $row->am_title ) );
+ }
+ return $arr;
+ }
+
+ function getCellAttrs( $field, $value ){
+ if( $this->mCurrentRow->am_customised && $field == 'am_title' ){
+ return array( 'rowspan' => '2', 'class' => $field );
+ } else {
+ return array( 'class' => $field );
+ }
+ }
+
+ // This is not actually used, as getStartBody is overridden above
+ function getFieldNames() {
+ return array(
+ 'am_title' => wfMsg( 'allmessagesname' ),
+ 'am_default' => wfMsg( 'allmessagesdefault' )
+ );
+ }
+ function getTitle() {
+ return SpecialPage::getTitleFor( 'Allmessages', false );
+ }
+ function isFieldSortable( $x ){
+ return false;
+ }
+ function getDefaultSort(){
+ return '';
+ }
+ function getQueryInfo(){
+ return '';
+ }
+}
+/* Overloads the relevant methods of the real ResultsWrapper so it
+ * doesn't go anywhere near an actual database.
+ */
+class FakeResultWrapper extends ResultWrapper {
+
+ var $result = array();
+ var $db = null; // And it's going to stay that way :D
+ var $pos = 0;
+ var $currentRow = null;
+
+ function __construct( $array ){
+ $this->result = $array;
+ }
+
+ function numRows() {
+ return count( $this->result );
+ }
+
+ function fetchRow() {
+ $this->currentRow = $this->result[$this->pos++];
+ return $this->currentRow;
+ }
+
+ function seek( $row ) {
+ $this->pos = $row;
+ }
+
+ function free() {}
+
+ // Callers want to be able to access fields with $this->fieldName
+ function fetchObject(){
+ $this->currentRow = $this->result[$this->pos++];
+ return (object)$this->currentRow;
+ }
+
+ function rewind() {
+ $this->pos = 0;
+ $this->currentRow = null;
+ }
}
diff --git a/includes/specials/SpecialAllpages.php b/includes/specials/SpecialAllpages.php
index bded8835..a36cdca7 100644
--- a/includes/specials/SpecialAllpages.php
+++ b/includes/specials/SpecialAllpages.php
@@ -14,7 +14,7 @@ class SpecialAllpages extends IncludableSpecialPage {
/**
* Maximum number of pages to show on single index subpage.
*/
- protected $maxLineCount = 200;
+ protected $maxLineCount = 100;
/**
* Maximum number of chars to show for an entry.
@@ -48,7 +48,8 @@ class SpecialAllpages extends IncludableSpecialPage {
$namespaces = $wgContLang->getNamespaces();
- $wgOut->setPagetitle( ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ?
+ $wgOut->setPagetitle(
+ ( $namespace > 0 && in_array( $namespace, array_keys( $namespaces) ) ) ?
wfMsg( 'allinnamespace', str_replace( '_', ' ', $namespaces[$namespace] ) ) :
wfMsg( 'allarticles' )
);
@@ -69,53 +70,52 @@ class SpecialAllpages extends IncludableSpecialPage {
* @param string $to dbKey we are ending listing at.
*/
function namespaceForm( $namespace = NS_MAIN, $from = '', $to = '' ) {
- global $wgScript;
- $t = $this->getTitle();
-
- $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
- $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
- $out .= Xml::hidden( 'title', $t->getPrefixedText() );
- $out .= Xml::openElement( 'fieldset' );
- $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
- $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
- $out .= "<tr>
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
- "</td>
- </tr>
- <tr>
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) .
- "</td>
- </tr>
- <tr>
- <td class='mw-label'>" .
- Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
- "</td>
- <td class='mw-input'>" .
- Xml::namespaceSelector( $namespace, null ) . ' ' .
- Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
- "</td>
- </tr>";
- $out .= Xml::closeElement( 'table' );
- $out .= Xml::closeElement( 'fieldset' );
- $out .= Xml::closeElement( 'form' );
- $out .= Xml::closeElement( 'div' );
- return $out;
+ global $wgScript;
+ $t = $this->getTitle();
+
+ $out = Xml::openElement( 'div', array( 'class' => 'namespaceoptions' ) );
+ $out .= Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) );
+ $out .= Xml::hidden( 'title', $t->getPrefixedText() );
+ $out .= Xml::openElement( 'fieldset' );
+ $out .= Xml::element( 'legend', null, wfMsg( 'allpages' ) );
+ $out .= Xml::openElement( 'table', array( 'id' => 'nsselect', 'class' => 'allpages' ) );
+ $out .= "<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) .
+ " </td>
+ <td class='mw-input'>" .
+ Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
+ " </td>
+</tr>
+<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) .
+ " </td>
+ <td class='mw-input'>" .
+ Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) .
+ " </td>
+</tr>
+<tr>
+ <td class='mw-label'>" .
+ Xml::label( wfMsg( 'namespace' ), 'namespace' ) .
+ " </td>
+ <td class='mw-input'>" .
+ Xml::namespaceSelector( $namespace, null ) . ' ' .
+ Xml::submitButton( wfMsg( 'allpagessubmit' ) ) .
+ " </td>
+</tr>";
+ $out .= Xml::closeElement( 'table' );
+ $out .= Xml::closeElement( 'fieldset' );
+ $out .= Xml::closeElement( 'form' );
+ $out .= Xml::closeElement( 'div' );
+ return $out;
}
/**
* @param integer $namespace (default NS_MAIN)
*/
function showToplevel( $namespace = NS_MAIN, $from = '', $to = '' ) {
- global $wgOut, $wgContLang;
- $align = $wgContLang->isRtl() ? 'left' : 'right';
+ global $wgOut;
# TODO: Either make this *much* faster or cache the title index points
# in the querycache table.
@@ -126,8 +126,8 @@ class SpecialAllpages extends IncludableSpecialPage {
$from = Title::makeTitleSafe( $namespace, $from );
$to = Title::makeTitleSafe( $namespace, $to );
- $from = ( $from && $from->isLocal() ) ? $from->getDBKey() : null;
- $to = ( $to && $to->isLocal() ) ? $to->getDBKey() : null;
+ $from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null;
+ $to = ( $to && $to->isLocal() ) ? $to->getDBkey() : null;
if( isset($from) )
$where[] = 'page_title >= '.$dbr->addQuotes( $from );
@@ -190,7 +190,7 @@ class SpecialAllpages extends IncludableSpecialPage {
// Instead, display the first section directly.
if( count( $lines ) <= 2 ) {
if( !empty($lines) ) {
- $this->showChunk( $namespace, $lines[0], $lines[count($lines)-1] );
+ $this->showChunk( $namespace, $from, $to );
} else {
$wgOut->addHTML( $this->namespaceForm( $namespace, $from, $to ) );
}
@@ -198,13 +198,13 @@ class SpecialAllpages extends IncludableSpecialPage {
}
# At this point, $lines should contain an even number of elements.
- $out .= "<table class='allpageslist' style='background: inherit;'>";
+ $out .= Xml::openElement( 'table', array( 'class' => 'allpageslist' ) );
while( count ( $lines ) > 0 ) {
$inpoint = array_shift( $lines );
$outpoint = array_shift( $lines );
$out .= $this->showline( $inpoint, $outpoint, $namespace );
}
- $out .= '</table>';
+ $out .= Xml::closeElement( 'table' );
$nsForm = $this->namespaceForm( $namespace, $from, $to );
# Is there more?
@@ -213,11 +213,17 @@ class SpecialAllpages extends IncludableSpecialPage {
} else {
if( isset($from) || isset($to) ) {
global $wgUser;
- $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $out2 .= '<tr valign="top"><td>' . $nsForm;
- $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
- $wgUser->getSkin()->makeKnownLinkObj( $this->getTitle(), wfMsgHtml ( 'allpages' ) );
- $out2 .= "</td></tr></table>";
+ $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ).
+ '<tr>
+ <td>' .
+ $nsForm .
+ '</td>
+ <td class="mw-allpages-nav">' .
+ $wgUser->getSkin()->link( $this->getTitle(), wfMsgHtml ( 'allpages' ),
+ array(), array(), 'known' ) .
+ "</td>
+ </tr>" .
+ Xml::closeElement( 'table' );
} else {
$out2 = $nsForm;
}
@@ -233,7 +239,6 @@ class SpecialAllpages extends IncludableSpecialPage {
*/
function showline( $inpoint, $outpoint, $namespace = NS_MAIN ) {
global $wgContLang;
- $align = $wgContLang->isRtl() ? 'left' : 'right';
$inpointf = htmlspecialchars( str_replace( '_', ' ', $inpoint ) );
$outpointf = htmlspecialchars( str_replace( '_', ' ', $outpoint ) );
// Don't let the length runaway
@@ -248,7 +253,7 @@ class SpecialAllpages extends IncludableSpecialPage {
"<a href=\"$link\">$inpointf</a></td><td>",
"</td><td><a href=\"$link\">$outpointf</a>"
);
- return '<tr><td align="' . $align . '">'.$out.'</td></tr>';
+ return '<tr><td class="mw-allpages-alphaindexline">' . $out . '</td></tr>';
}
/**
@@ -264,8 +269,6 @@ class SpecialAllpages extends IncludableSpecialPage {
$fromList = $this->getNamespaceKeyAndText($namespace, $from);
$toList = $this->getNamespaceKeyAndText( $namespace, $to );
$namespaces = $wgContLang->getNamespaces();
- $align = $wgContLang->isRtl() ? 'left' : 'right';
-
$n = 0;
if ( !$fromList || !$toList ) {
@@ -299,13 +302,12 @@ class SpecialAllpages extends IncludableSpecialPage {
);
if( $res->numRows() > 0 ) {
- $out = '<table style="background: inherit;" border="0" width="100%">';
-
+ $out = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-chunk' ) );
while( ( $n < $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
$t = Title::makeTitle( $s->page_namespace, $s->page_title );
if( $t ) {
$link = ( $s->page_is_redirect ? '<div class="allpagesredirect">' : '' ) .
- $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) .
+ $sk->linkKnown( $t, htmlspecialchars( $t->getText() ) ) .
($s->page_is_redirect ? '</div>' : '' );
} else {
$link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
@@ -316,13 +318,13 @@ class SpecialAllpages extends IncludableSpecialPage {
$out .= "<td width=\"33%\">$link</td>";
$n++;
if( $n % 3 == 0 ) {
- $out .= '</tr>';
+ $out .= "</tr>\n";
}
}
if( ($n % 3) != 0 ) {
- $out .= '</tr>';
+ $out .= "</tr>\n";
}
- $out .= '</table>';
+ $out .= Xml::closeElement( 'table' );
} else {
$out = '';
}
@@ -342,7 +344,9 @@ class SpecialAllpages extends IncludableSpecialPage {
'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 ) )
+ array( 'ORDER BY' => 'page_title DESC',
+ 'LIMIT' => $this->maxPerPage, 'OFFSET' => ($this->maxPerPage - 1 )
+ )
);
# Get first title of previous complete chunk
@@ -370,28 +374,44 @@ class SpecialAllpages extends IncludableSpecialPage {
$self = $this->getTitle();
$nsForm = $this->namespaceForm( $namespace, $from, $to );
- $out2 = '<table style="background: inherit;" width="100%" cellpadding="0" cellspacing="0" border="0">';
- $out2 .= '<tr valign="top"><td>' . $nsForm;
- $out2 .= '</td><td align="' . $align . '" style="font-size: smaller; margin-bottom: 1em;">' .
- $sk->makeKnownLinkObj( $self,
- wfMsgHtml ( 'allpages' ) );
+ $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ).
+ '<tr>
+ <td>' .
+ $nsForm .
+ '</td>
+ <td class="mw-allpages-nav">' .
+ $sk->link( $self, wfMsgHtml ( 'allpages' ), array(), array(), 'known' );
# 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 );
+ $query = array( 'from' => $prevTitle->getText() );
+
+ if( $namespace )
+ $query['namespace'] = $namespace;
+
+ $prevLink = $sk->linkKnown(
+ $self,
+ htmlspecialchars( wfMsg( 'prevpage', $pt ) ),
+ array(),
+ $query
+ );
$out2 = $wgLang->pipeList( array( $out2, $prevLink ) );
}
if( $n == $this->maxPerPage && $s = $res->fetchObject() ) {
# $s is the first link of the next chunk
$t = Title::MakeTitle($namespace, $s->page_title);
- $q = 'from=' . $t->getPartialUrl()
- . ( $namespace ? '&namespace=' . $namespace : '' );
- $nextLink = $sk->makeKnownLinkObj( $self,
- wfMsgHtml( 'nextpage', htmlspecialchars( $t->getText() ) ), $q );
+ $query = array( 'from' => $t->getText() );
+
+ if( $namespace )
+ $query['namespace'] = $namespace;
+
+ $nextLink = $sk->linkKnown(
+ $self,
+ htmlspecialchars( wfMsg( 'nextpage', $t->getText() ) ),
+ array(),
+ $query
+ );
$out2 = $wgLang->pipeList( array( $out2, $nextLink ) );
}
$out2 .= "</td></tr></table>";
@@ -399,7 +419,7 @@ class SpecialAllpages extends IncludableSpecialPage {
$wgOut->addHTML( $out2 . $out );
if( isset($prevLink) or isset($nextLink) ) {
- $wgOut->addHTML( '<hr /><p style="font-size: smaller; float: ' . $align . '">' );
+ $wgOut->addHTML( '<hr /><p class="mw-allpages-nav">' );
if( isset( $prevLink ) ) {
$wgOut->addHTML( $prevLink );
}
@@ -430,7 +450,7 @@ class SpecialAllpages extends IncludableSpecialPage {
if ( $t && $t->isLocal() ) {
return array( $t->getNamespace(), $t->getDBkey(), $t->getText() );
} else if ( $t ) {
- return NULL;
+ return null;
}
# try again, in case the problem was an empty pagename
@@ -439,7 +459,7 @@ class SpecialAllpages extends IncludableSpecialPage {
if ( $t && $t->isLocal() ) {
return array( $t->getNamespace(), '', '' );
} else {
- return NULL;
+ return null;
}
}
}
diff --git a/includes/specials/SpecialAncientpages.php b/includes/specials/SpecialAncientpages.php
index 188ad914..92192435 100644
--- a/includes/specials/SpecialAncientpages.php
+++ b/includes/specials/SpecialAncientpages.php
@@ -25,8 +25,25 @@ class AncientPagesPage extends QueryPage {
$db = wfGetDB( DB_SLAVE );
$page = $db->tableName( 'page' );
$revision = $db->tableName( 'revision' );
- $epoch = $wgDBtype == 'mysql' ? 'UNIX_TIMESTAMP(rev_timestamp)' :
- 'EXTRACT(epoch FROM rev_timestamp)';
+
+ switch ($wgDBtype) {
+ case 'mysql':
+ $epoch = 'UNIX_TIMESTAMP(rev_timestamp)';
+ break;
+ case 'ibm_db2':
+ // TODO implement proper conversion to a Unix epoch
+ $epoch = 'rev_timestamp';
+ break;
+ case 'oracle':
+ $epoch = '((trunc(rev_timestamp) - to_date(\'19700101\',\'YYYYMMDD\')) * 86400)';
+ break;
+ case 'sqlite':
+ $epoch = 'rev_timestamp';
+ break;
+ default:
+ $epoch = 'EXTRACT(epoch FROM rev_timestamp)';
+ }
+
return
"SELECT 'Ancientpages' as type,
page_namespace as namespace,
@@ -46,8 +63,11 @@ class AncientPagesPage extends QueryPage {
$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);
+ $link = $skin->linkKnown(
+ $title,
+ htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) )
+ );
+ return wfSpecialList($link, htmlspecialchars($d) );
}
}
diff --git a/includes/specials/SpecialBlankpage.php b/includes/specials/SpecialBlankpage.php
index 29d6b96c..e1fadd02 100644
--- a/includes/specials/SpecialBlankpage.php
+++ b/includes/specials/SpecialBlankpage.php
@@ -1,6 +1,17 @@
<?php
-
-function wfSpecialBlankpage() {
- global $wgOut;
- $wgOut->addWikiMsg('intentionallyblankpage');
+/**
+ * Special page designed for basic benchmarking of
+ * MediaWiki since it doesn't really do much.
+ *
+ * @ingroup SpecialPage
+ */
+class SpecialBlankpage extends UnlistedSpecialPage {
+ public function __construct() {
+ parent::__construct( 'Blankpage' );
+ }
+ public function execute( $par ) {
+ global $wgOut;
+ $this->setHeaders();
+ $wgOut->addWikiMsg('intentionallyblankpage');
+ }
}
diff --git a/includes/specials/SpecialBlockip.php b/includes/specials/SpecialBlockip.php
index f002e570..16720dd1 100644
--- a/includes/specials/SpecialBlockip.php
+++ b/includes/specials/SpecialBlockip.php
@@ -17,7 +17,6 @@ function wfSpecialBlockip( $par ) {
$wgOut->readOnlyPage();
return;
}
-
# Permission check
if( !$wgUser->isAllowed( 'block' ) ) {
$wgOut->permissionRequired( 'block' );
@@ -27,9 +26,9 @@ function wfSpecialBlockip( $par ) {
$ipb = new IPBlockForm( $par );
$action = $wgRequest->getVal( 'action' );
- if ( 'success' == $action ) {
+ if( 'success' == $action ) {
$ipb->showSuccess();
- } else if ( $wgRequest->wasPosted() && 'submit' == $action &&
+ } elseif( $wgRequest->wasPosted() && 'submit' == $action &&
$wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) {
$ipb->doSubmit();
} else {
@@ -44,18 +43,17 @@ function wfSpecialBlockip( $par ) {
*/
class IPBlockForm {
var $BlockAddress, $BlockExpiry, $BlockReason;
-# var $BlockEmail;
// The maximum number of edits a user can have and still be hidden
const HIDEUSER_CONTRIBLIMIT = 1000;
- function IPBlockForm( $par ) {
+ public function __construct( $par ) {
global $wgRequest, $wgUser, $wgBlockAllowsUTEdit;
$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->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
@@ -64,21 +62,30 @@ class IPBlockForm {
$this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly', $byDefault );
$this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', $byDefault );
$this->BlockEnableAutoblock = $wgRequest->getBool( 'wpEnableAutoblock', $byDefault );
- $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false );
- $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false );
- # Re-check user's rights to hide names, very serious, defaults to 0
- $this->BlockHideName = ( $wgRequest->getBool( 'wpHideName', 0 ) && $wgUser->isAllowed( 'hideuser' ) ) ? 1 : 0;
+ $this->BlockEmail = false;
+ if( self::canBlockEmail( $wgUser ) ) {
+ $this->BlockEmail = $wgRequest->getBool( 'wpEmailBan', false );
+ }
+ $this->BlockWatchUser = $wgRequest->getBool( 'wpWatchUser', false ) && $wgUser->isLoggedIn();
+ # Re-check user's rights to hide names, very serious, defaults to null
+ if( $wgUser->isAllowed( 'hideuser' ) ) {
+ $this->BlockHideName = $wgRequest->getBool( 'wpHideName', null );
+ } else {
+ $this->BlockHideName = false;
+ }
$this->BlockAllowUsertalk = ( $wgRequest->getBool( 'wpAllowUsertalk', $byDefault ) && $wgBlockAllowsUTEdit );
$this->BlockReblock = $wgRequest->getBool( 'wpChangeBlock', false );
+
+ $this->wasPosted = $wgRequest->wasPosted();
}
- function showForm( $err ) {
+ public function showForm( $err ) {
global $wgOut, $wgUser, $wgSysopUserBans;
- $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->setPageTitle( wfMsg( 'blockip-title' ) );
$wgOut->addWikiMsg( 'blockiptext' );
- if($wgSysopUserBans) {
+ if( $wgSysopUserBans ) {
$mIpaddress = Xml::label( wfMsg( 'ipadressorusername' ), 'mw-bi-target' );
} else {
$mIpaddress = Xml::label( wfMsg( 'ipaddress' ), 'mw-bi-target' );
@@ -90,25 +97,28 @@ class IPBlockForm {
$titleObj = SpecialPage::getTitleFor( 'Blockip' );
$user = User::newFromName( $this->BlockAddress );
-
+
$alreadyBlocked = false;
- if ( $err && $err[0] != 'ipb_already_blocked' ) {
- $key = array_shift($err);
- $msg = wfMsgReal($key, $err);
+ $otherBlockedMsgs = array();
+ if( $err && $err[0] != 'ipb_already_blocked' ) {
+ $key = array_shift( $err );
+ $msg = wfMsgReal( $key, $err );
$wgOut->setSubtitle( wfMsgHtml( 'formerror' ) );
$wgOut->addHTML( Xml::tags( 'p', array( 'class' => 'error' ), $msg ) );
- } elseif ( $this->BlockAddress ) {
- $userId = 0;
- if ( is_object( $user ) )
- $userId = $user->getId();
+ } elseif( $this->BlockAddress ) {
+ # Get other blocks, i.e. from GlobalBlocking or TorBlock extension
+ wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockedMsgs, $this->BlockAddress ) );
+
+ $userId = is_object( $user ) ? $user->getId() : 0;
$currentBlock = Block::newFromDB( $this->BlockAddress, $userId );
- if ( !is_null($currentBlock) && !$currentBlock->mAuto && # The block exists and isn't an autoblock
+ if( !is_null( $currentBlock ) && !$currentBlock->mAuto && # The block exists and isn't an autoblock
( $currentBlock->mRangeStart == $currentBlock->mRangeEnd || # The block isn't a rangeblock
# or if it is, the range is what we're about to block
- ( $currentBlock->mAddress == $this->BlockAddress ) ) ) {
- $wgOut->addWikiMsg( 'ipb-needreblock', $this->BlockAddress );
- $alreadyBlocked = true;
- # Set the block form settings to the existing block
+ ( $currentBlock->mAddress == $this->BlockAddress ) )
+ ) {
+ $alreadyBlocked = true;
+ # Set the block form settings to the existing block
+ if( !$this->wasPosted ) {
$this->BlockAnonOnly = $currentBlock->mAnonOnly;
$this->BlockCreateAccount = $currentBlock->mCreateAccount;
$this->BlockEnableAutoblock = $currentBlock->mEnableAutoblock;
@@ -121,21 +131,38 @@ class IPBlockForm {
$this->BlockOther = wfTimestamp( TS_ISO_8601, $currentBlock->mExpiry );
}
$this->BlockReason = $currentBlock->mReason;
+ }
+ }
+ }
+
+ # Show other blocks from extensions, i.e. GlockBlocking and TorBlock
+ if( count( $otherBlockedMsgs ) ) {
+ $wgOut->addHTML(
+ Html::rawElement( 'h2', array(), wfMsgExt( 'ipb-otherblocks-header', 'parseinline', count( $otherBlockedMsgs ) ) ) . "\n"
+ );
+ $list = '';
+ foreach( $otherBlockedMsgs as $link ) {
+ $list .= Html::rawElement( 'li', array(), $link ) . "\n";
}
+ $wgOut->addHTML( Html::rawElement( 'ul', array( 'class' => 'mw-blockip-alreadyblocked' ), $list ) . "\n" );
+ }
+
+ # Username/IP is blocked already locally
+ if( $alreadyBlocked ) {
+ $wgOut->addWikiMsg( 'ipb-needreblock', $this->BlockAddress );
}
$scBlockExpiryOptions = wfMsgForContent( 'ipboptions' );
$showblockoptions = $scBlockExpiryOptions != '-';
- if (!$showblockoptions)
- $mIpbother = $mIpbexpiry;
+ 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);
+ 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";
}
@@ -146,25 +173,27 @@ class IPBlockForm {
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( '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' ) ) .
+ 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()' ) ). "
+ Html::input( 'wpBlockAddress', $this->BlockAddress, 'text', array(
+ 'tabindex' => '1',
+ 'id' => 'mw-bi-target',
+ 'onchange' => 'updateBlockOptions()',
+ 'size' => '45',
+ 'required' => ''
+ ) + ( $this->BlockAddress ? array() : array( 'autofocus' ) ) ). "
</td>
</tr>
<tr>"
);
- if ( $showblockoptions ) {
+ if( $showblockoptions ) {
$wgOut->addHTML("
<td class='mw-label'>
{$mIpbexpiry}
@@ -204,8 +233,12 @@ class IPBlockForm {
{$mIpbreason}
</td>
<td class='mw-input'>" .
- Xml::input( 'wpBlockReason', 45, $this->BlockReason,
- array( 'tabindex' => '5', 'id' => 'mw-bi-reason', 'maxlength'=> '200' ) ) . "
+ Html::input( 'wpBlockReason', $this->BlockReason, 'text', array(
+ 'tabindex' => '5',
+ 'id' => 'mw-bi-reason',
+ 'maxlength' => '200',
+ 'size' => '45'
+ ) + ( $this->BlockAddress ? array( 'autofocus' ) : array() ) ) . "
</td>
</tr>
<tr id='wpAnonOnlyRow'>
@@ -234,36 +267,37 @@ class IPBlockForm {
</tr>"
);
- global $wgSysopEmailBans, $wgBlockAllowsUTEdit;
- if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) {
+ if( self::canBlockEmail( $wgUser ) ) {
$wgOut->addHTML("
<tr id='wpEnableEmailBan'>
<td>&nbsp;</td>
<td class='mw-input'>" .
Xml::checkLabel( wfMsg( 'ipbemailban' ),
'wpEmailBan', 'wpEmailBan', $this->BlockEmail,
- array( 'tabindex' => '9' )) . "
+ array( 'tabindex' => '9' ) ) . "
</td>
</tr>"
);
}
// Allow some users to hide name from block log, blocklist and listusers
- if ( $wgUser->isAllowed( 'hideuser' ) ) {
+ if( $wgUser->isAllowed( 'hideuser' ) ) {
$wgOut->addHTML("
<tr id='wpEnableHideUser'>
<td>&nbsp;</td>
<td class='mw-input'><strong>" .
Xml::checkLabel( wfMsg( 'ipbhidename' ),
'wpHideName', 'wpHideName', $this->BlockHideName,
- array( 'tabindex' => '10' ) ) . "
+ array( 'tabindex' => '10' )
+ ) . "
</strong></td>
</tr>"
);
}
-
- # Watchlist their user page?
- $wgOut->addHTML("
+
+ # Watchlist their user page? (Only if user is logged in)
+ if( $wgUser->isLoggedIn() ) {
+ $wgOut->addHTML("
<tr id='wpEnableWatchUser'>
<td>&nbsp;</td>
<td class='mw-input'>" .
@@ -272,7 +306,11 @@ class IPBlockForm {
array( 'tabindex' => '11' ) ) . "
</td>
</tr>"
- );
+ );
+ }
+
+ # Can we explicitly disallow the use of user_talk?
+ global $wgBlockAllowsUTEdit;
if( $wgBlockAllowsUTEdit ){
$wgOut->addHTML("
<tr id='wpAllowUsertalkRow'>
@@ -314,12 +352,22 @@ class IPBlockForm {
}
/**
+ * Can we do an email block?
+ * @param User $user The sysop wanting to make a block
+ * @return boolean
+ */
+ public static function canBlockEmail( $user ) {
+ global $wgEnableUserEmail, $wgSysopEmailBans;
+ return ( $wgEnableUserEmail && $wgSysopEmailBans && $user->isAllowed( 'blockemail' ) );
+ }
+
+ /**
* 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, $wgBlockAllowsUTEdit;
+ global $wgUser, $wgSysopUserBans, $wgSysopRangeBans, $wgBlockAllowsUTEdit, $wgBlockCIDRLimit;
$userId = 0;
# Expand valid IPv6 addresses, usernames are left as is
@@ -330,24 +378,28 @@ class IPBlockForm {
$rxIP = "($rxIP4|$rxIP6)";
# Check for invalid specifications
- if ( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
+ if( !preg_match( "/^$rxIP$/", $this->BlockAddress ) ) {
$matches = array();
- if ( preg_match( "/^($rxIP4)\\/(\\d{1,2})$/", $this->BlockAddress, $matches ) ) {
+ 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');
+ if( $wgSysopRangeBans ) {
+ if( !IP::isIPv4( $this->BlockAddress ) || $matches[2] > 32 ) {
+ return array( 'ip_range_invalid' );
+ } elseif ( $matches[2] < $wgBlockCIDRLimit['IPv4'] ) {
+ return array( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] );
}
$this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
} else {
# Range block illegal
- return array('range_block_disabled');
+ return array( 'range_block_disabled' );
}
- } else if ( preg_match( "/^($rxIP6)\\/(\\d{1,3})$/", $this->BlockAddress, $matches ) ) {
+ } elseif( 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');
+ if( $wgSysopRangeBans ) {
+ if( !IP::isIPv6( $this->BlockAddress ) || $matches[2] > 128 ) {
+ return array( 'ip_range_invalid' );
+ } elseif( $matches[2] < $wgBlockCIDRLimit['IPv6'] ) {
+ return array( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] );
}
$this->BlockAddress = Block::normaliseRange( $this->BlockAddress );
} else {
@@ -356,30 +408,30 @@ class IPBlockForm {
}
} else {
# Username block
- if ( $wgSysopUserBans ) {
+ 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 ) );
+ return array( 'nosuchusershort', htmlspecialchars( $user ? $user->getName() : $this->BlockAddress ) );
}
} else {
- return array('badipaddress');
+ return array( 'badipaddress' );
}
}
}
- if ( $wgUser->isBlocked() && ( $wgUser->getId() !== $userId ) ) {
+ if( $wgUser->isBlocked() && ( $wgUser->getId() !== $userId ) ) {
return array( 'cant-block-while-blocked' );
}
$reasonstr = $this->BlockReasonList;
- if ( $reasonstr != 'other' && $this->BlockReason != '' ) {
+ if( $reasonstr != 'other' && $this->BlockReason != '' ) {
// Entry from drop down menu + additional comment
$reasonstr .= wfMsgForContent( 'colon-separator' ) . $this->BlockReason;
- } elseif ( $reasonstr == 'other' ) {
+ } elseif( $reasonstr == 'other' ) {
$reasonstr = $this->BlockReason;
}
@@ -387,44 +439,45 @@ class IPBlockForm {
if( $expirestr == 'other' )
$expirestr = $this->BlockOther;
- if ( ( strlen( $expirestr ) == 0) || ( strlen( $expirestr ) > 50) ) {
- return array('ipb_expiry_invalid');
+ if( ( strlen( $expirestr ) == 0) || ( strlen( $expirestr ) > 50 ) ) {
+ return array( 'ipb_expiry_invalid' );
}
- if ( false === ($expiry = Block::parseExpiryInput( $expirestr )) ) {
+ if( false === ( $expiry = Block::parseExpiryInput( $expirestr ) ) ) {
// Bad expiry.
- return array('ipb_expiry_invalid');
+ return array( 'ipb_expiry_invalid' );
}
-
+
if( $this->BlockHideName ) {
- if( !$userId ) {
- // IP users should not be hidden
- $this->BlockHideName = false;
- } else if( $expiry !== 'infinity' ) {
+ // Recheck params here...
+ if( !$userId || !$wgUser->isAllowed('hideuser') ) {
+ $this->BlockHideName = false; // IP users should not be hidden
+ } elseif( $expiry !== 'infinity' ) {
// Bad expiry.
- return array('ipb_expiry_temp');
- } else if( User::edits($userId) > self::HIDEUSER_CONTRIBLIMIT ) {
+ return array( 'ipb_expiry_temp' );
+ } elseif( User::edits( $userId ) > self::HIDEUSER_CONTRIBLIMIT ) {
// Typically, the user should have a handful of edits.
// Disallow hiding users with many edits for performance.
- return array('ipb_hide_invalid');
+ return array( 'ipb_hide_invalid' );
}
}
- # Create block
+ # Create block object
# 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, isset( $this->BlockAllowUsertalk ) ? $this->BlockAllowUsertalk : $wgBlockAllowsUTEdit
+ $this->BlockEmail,
+ isset( $this->BlockAllowUsertalk ) ? $this->BlockAllowUsertalk : $wgBlockAllowsUTEdit
);
# Should this be privately logged?
$suppressLog = (bool)$this->BlockHideName;
- if ( wfRunHooks('BlockIp', array(&$block, &$wgUser)) ) {
+ if( wfRunHooks( 'BlockIp', array( &$block, &$wgUser ) ) ) {
# Try to insert block. Is there a conflicting block?
- if ( !$block->insert() ) {
+ if( !$block->insert() ) {
# Show form unless the user is already aware of this...
- if ( !$this->BlockReblock ) {
+ if( !$this->BlockReblock ) {
return array( 'ipb_already_blocked' );
# Otherwise, try to update the block...
} else {
@@ -436,8 +489,8 @@ class IPBlockForm {
}
# If the name was hidden and the blocking user cannot hide
# names, then don't allow any block changes...
- if( $currentBlock->mHideName && !$wgUser->isAllowed('hideuser') ) {
- return array( 'hookaborted' );
+ if( $currentBlock->mHideName && !$wgUser->isAllowed( 'hideuser' ) ) {
+ return array( 'cant-see-hidden-user' );
}
$currentBlock->delete();
$block->insert();
@@ -452,19 +505,18 @@ class IPBlockForm {
} else {
$log_action = 'block';
}
- wfRunHooks('BlockIpComplete', array($block, $wgUser));
+ wfRunHooks( 'BlockIpComplete', array( $block, $wgUser ) );
# Set *_deleted fields if requested
if( $this->BlockHideName ) {
self::suppressUserName( $this->BlockAddress, $userId );
}
- if ( $this->BlockWatchUser &&
- # Only show watch link when this is no range block
- $block->mRangeStart == $block->mRangeEnd) {
- $wgUser->addWatch ( Title::makeTitle( NS_USER, $this->BlockAddress ) );
+ # Only show watch link when this is no range block
+ if( $this->BlockWatchUser && $block->mRangeStart == $block->mRangeEnd ) {
+ $wgUser->addWatch( Title::makeTitle( NS_USER, $this->BlockAddress ) );
}
-
+
# Block constructor sanitizes certain block options on insert
$this->BlockEmail = $block->mBlockEmail;
$this->BlockEnableAutoblock = $block->mEnableAutoblock;
@@ -478,34 +530,34 @@ class IPBlockForm {
$log_type = $suppressLog ? 'suppress' : 'block';
$log = new LogPage( $log_type );
$log->addEntry( $log_action, Title::makeTitle( NS_USER, $this->BlockAddress ),
- $reasonstr, $logParams );
+ $reasonstr, $logParams );
# Report to the user
return array();
} else {
- return array('hookaborted');
+ return array( 'hookaborted' );
}
}
-
- public static function suppressUserName( $name, $userId ) {
+
+ public static function suppressUserName( $name, $userId, $dbw = null ) {
$op = '|'; // bitwise OR
- return self::setUsernameBitfields( $name, $userId, $op );
+ return self::setUsernameBitfields( $name, $userId, $op, $dbw );
}
-
- public static function unsuppressUserName( $name, $userId ) {
+
+ public static function unsuppressUserName( $name, $userId, $dbw = null ) {
$op = '&'; // bitwise AND
- return self::setUsernameBitfields( $name, $userId, $op );
+ return self::setUsernameBitfields( $name, $userId, $op, $dbw );
}
-
- private static function setUsernameBitfields( $name, $userId, $op ) {
- if( $op !== '|' && $op !== '&' )
- return false; // sanity check
- $dbw = wfGetDB( DB_MASTER );
+
+ private static function setUsernameBitfields( $name, $userId, $op, $dbw ) {
+ if( $op !== '|' && $op !== '&' ) return false; // sanity check
+ if( !$dbw )
+ $dbw = wfGetDB( DB_MASTER );
$delUser = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
$delAction = LogPage::DELETED_ACTION | Revision::DELETED_RESTRICTED;
# Normalize user name
$userTitle = Title::makeTitleSafe( NS_USER, $name );
- $userDbKey = $userTitle->getDBKey();
+ $userDbKey = $userTitle->getDBkey();
# To suppress, we OR the current bitfields with Revision::DELETED_USER
# to put a 1 in the username *_deleted bit. To unsuppress we AND the
# current bitfields with the inverse of Revision::DELETED_USER. The
@@ -516,27 +568,29 @@ class IPBlockForm {
$delAction = "~{$delAction}";
}
# Hide name from live edits
- $dbw->update( 'revision', array("rev_deleted = rev_deleted $op $delUser"),
- array('rev_user' => $userId), __METHOD__ );
+ $dbw->update( 'revision', array( "rev_deleted = rev_deleted $op $delUser" ),
+ array( 'rev_user' => $userId ), __METHOD__ );
# Hide name from deleted edits
- $dbw->update( 'archive', array("ar_deleted = ar_deleted $op $delUser"),
- array('ar_user_text' => $name), __METHOD__ );
+ $dbw->update( 'archive', array( "ar_deleted = ar_deleted $op $delUser" ),
+ array( 'ar_user_text' => $name ), __METHOD__ );
# Hide name from logs
- $dbw->update( 'logging', array("log_deleted = log_deleted $op $delUser"),
- array('log_user' => $userId, "log_type != 'suppress'"), __METHOD__ );
- $dbw->update( 'logging', array("log_deleted = log_deleted $op $delAction"),
- array('log_namespace' => NS_USER, 'log_title' => $userDbKey,
- "log_type != 'suppress'"), __METHOD__ );
+ $dbw->update( 'logging', array( "log_deleted = log_deleted $op $delUser" ),
+ array( 'log_user' => $userId, "log_type != 'suppress'" ), __METHOD__ );
+ $dbw->update( 'logging', array( "log_deleted = log_deleted $op $delAction" ),
+ array( 'log_namespace' => NS_USER, 'log_title' => $userDbKey,
+ "log_type != 'suppress'" ), __METHOD__ );
# Hide name from RC
- $dbw->update( 'recentchanges', array("rc_deleted = rc_deleted $op $delUser"),
- array('rc_user_text' => $name), __METHOD__ );
+ $dbw->update( 'recentchanges', array( "rc_deleted = rc_deleted $op $delUser" ),
+ array( 'rc_user_text' => $name ), __METHOD__ );
+ $dbw->update( 'recentchanges', array( "rc_deleted = rc_deleted $op $delAction" ),
+ array( 'rc_namespace' => NS_USER, 'rc_title' => $userDbKey, 'rc_logid > 0' ), __METHOD__ );
# Hide name from live images
- $dbw->update( 'oldimage', array("oi_deleted = oi_deleted $op $delUser"),
- array('oi_user_text' => $name), __METHOD__ );
+ $dbw->update( 'oldimage', array( "oi_deleted = oi_deleted $op $delUser" ),
+ array( 'oi_user_text' => $name ), __METHOD__ );
# Hide name from deleted images
# WMF - schema change pending
- # $dbw->update( 'filearchive', array("fa_deleted = fa_deleted $op $delUser"),
- # array('fa_user_text' => $name), __METHOD__ );
+ # $dbw->update( 'filearchive', array( "fa_deleted = fa_deleted $op $delUser" ),
+ # array( 'fa_user_text' => $name ), __METHOD__ );
# Done!
return true;
}
@@ -545,11 +599,10 @@ class IPBlockForm {
* UI entry point for blocking
* Wraps around doBlock()
*/
- function doSubmit()
- {
+ public function doSubmit() {
global $wgOut;
$retval = $this->doBlock();
- if(empty($retval)) {
+ if( empty( $retval ) ) {
$titleObj = SpecialPage::getTitleFor( 'Blockip' );
$wgOut->redirect( $titleObj->getFullURL( 'action=success&ip=' .
urlencode( $this->BlockAddress ) ) );
@@ -558,27 +611,55 @@ class IPBlockForm {
$this->showForm( $retval );
}
- function showSuccess() {
+ public function showSuccess() {
global $wgOut;
- $wgOut->setPagetitle( wfMsg( 'blockip' ) );
+ $wgOut->setPageTitle( wfMsg( 'blockip-title' ) );
$wgOut->setSubtitle( wfMsg( 'blockipsuccesssub' ) );
$text = wfMsgExt( 'blockipsuccesstext', array( 'parse' ), $this->BlockAddress );
$wgOut->addHTML( $text );
}
- function showLogFragment( $out, $title ) {
+ private function showLogFragment( $out, $title ) {
global $wgUser;
- $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'block' ) ) );
- $count = LogEventsList::showLogExtract( $out, 'block', $title->getPrefixedText(), '', 10 );
- if($count > 10){
- $out->addHTML( $wgUser->getSkin()->link(
- SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'blocklog-fulllog' ),
- array(),
+
+ // Used to support GENDER in 'blocklog-showlog' and 'blocklog-showsuppresslog'
+ $userBlocked = $title->getText();
+
+ LogEventsList::showLogExtract(
+ $out,
+ 'block',
+ $title->getPrefixedText(),
+ '',
+ array(
+ 'lim' => 10,
+ 'msgKey' => array(
+ 'blocklog-showlog',
+ $userBlocked
+ ),
+ 'showIfEmpty' => false
+ )
+ );
+
+ // Add suppression block entries if allowed
+ if( $wgUser->isAllowed( 'hideuser' ) ) {
+ LogEventsList::showLogExtract( $out, 'suppress', $title->getPrefixedText(), '',
array(
- 'type' => 'block',
- 'page' => $title->getPrefixedText() ) ) );
+ 'lim' => 10,
+ 'conds' => array(
+ 'log_action' => array(
+ 'block',
+ 'reblock',
+ 'unblock'
+ )
+ ),
+ 'msgKey' => array(
+ 'blocklog-showsuppresslog',
+ $userBlocked
+ ),
+ 'showIfEmpty' => false
+ )
+ );
}
}
@@ -596,13 +677,14 @@ class IPBlockForm {
$flags[] = 'anononly';
if( $this->BlockCreateAccount )
$flags[] = 'nocreate';
- if( !$this->BlockEnableAutoblock )
+ if( !$this->BlockEnableAutoblock && !IP::isIPAddress( $this->BlockAddress ) )
+ // Same as anononly, this is not displayed when blocking an IP address
$flags[] = 'noautoblock';
- if ( $this->BlockEmail )
+ if( $this->BlockEmail )
$flags[] = 'noemail';
- if ( !$this->BlockAllowUsertalk && $wgBlockAllowsUTEdit )
+ if( !$this->BlockAllowUsertalk && $wgBlockAllowsUTEdit )
$flags[] = 'nousertalk';
- if ( $this->BlockHideName )
+ if( $this->BlockHideName )
$flags[] = 'hiddenname';
return implode( ',', $flags );
}
@@ -619,10 +701,18 @@ class IPBlockForm {
$links[] = $this->getContribsLink( $skin );
$links[] = $this->getUnblockLink( $skin );
$links[] = $this->getBlockListLink( $skin );
- $links[] = $skin->makeLink ( 'MediaWiki:Ipbreason-dropdown', wfMsgHtml( 'ipb-edit-dropdown' ) );
+ if ( $wgUser->isAllowed( 'editinterface' ) ) {
+ $title = Title::makeTitle( NS_MEDIAWIKI, 'Ipbreason-dropdown' );
+ $links[] = $skin->link(
+ $title,
+ wfMsgHtml( 'ipb-edit-dropdown' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
+ }
return '<p class="mw-ipb-conveniencelinks">' . $wgLang->pipeList( $links ) . '</p>';
}
-
+
/**
* Build a convenient link to a user or IP's contribs
* form
@@ -645,13 +735,21 @@ class IPBlockForm {
*/
private function getUnblockLink( $skin ) {
$list = SpecialPage::getTitleFor( 'Ipblocklist' );
+ $query = array( 'action' => 'unblock' );
+
if( $this->BlockAddress ) {
- $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
- return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock-addr', $addr ),
- 'action=unblock&ip=' . urlencode( $this->BlockAddress ) );
+ $addr = strtr( $this->BlockAddress, '_', ' ' );
+ $message = wfMsg( 'ipb-unblock-addr', $addr );
+ $query['ip'] = $this->BlockAddress;
} else {
- return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-unblock' ), 'action=unblock' );
+ $message = wfMsg( 'ipb-unblock' );
}
+ return $skin->linkKnown(
+ $list,
+ htmlspecialchars( $message ),
+ array(),
+ $query
+ );
}
/**
@@ -662,23 +760,32 @@ class IPBlockForm {
*/
private function getBlockListLink( $skin ) {
$list = SpecialPage::getTitleFor( 'Ipblocklist' );
+ $query = array();
+
if( $this->BlockAddress ) {
- $addr = htmlspecialchars( strtr( $this->BlockAddress, '_', ' ' ) );
- return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist-addr', $addr ),
- 'ip=' . urlencode( $this->BlockAddress ) );
+ $addr = strtr( $this->BlockAddress, '_', ' ' );
+ $message = wfMsg( 'ipb-blocklist-addr', $addr );
+ $query['ip'] = $this->BlockAddress;
} else {
- return $skin->makeKnownLinkObj( $list, wfMsgHtml( 'ipb-blocklist' ) );
+ $message = wfMsg( 'ipb-blocklist' );
}
+
+ return $skin->linkKnown(
+ $list,
+ htmlspecialchars( $message ),
+ array(),
+ $query
+ );
}
-
+
/**
- * Block a list of selected users
- * @param array $users
- * @param string $reason
- * @param string $tag replaces user pages
- * @param string $talkTag replaces user talk pages
- * @returns array, list of html-safe usernames
- */
+ * Block a list of selected users
+ * @param array $users
+ * @param string $reason
+ * @param string $tag replaces user pages
+ * @param string $talkTag replaces user talk pages
+ * @returns array, list of html-safe usernames
+ */
public static function doMassUserBlock( $users, $reason = '', $tag = '', $talkTag = '' ) {
global $wgUser;
$counter = $blockSize = 0;
@@ -695,7 +802,7 @@ class IPBlockForm {
}
$u = User::newFromName( $name, false );
// If user doesn't exist, it ought to be an IP then
- if( is_null($u) || (!$u->getId() && !IP::isIPAddress( $u->getName() )) ) {
+ if( is_null( $u ) || ( !$u->getId() && !IP::isIPAddress( $u->getName() ) ) ) {
continue;
}
$userTitle = $u->getUserPage();
@@ -734,10 +841,10 @@ class IPBlockForm {
$log->addEntry( 'block', $userTitle, $reason, $logParams );
}
# Tag userpage! (check length to avoid mistakes)
- if( strlen($tag) > 2 ) {
+ if( strlen( $tag ) > 2 ) {
$userpage->doEdit( $tag, $reason, EDIT_MINOR );
}
- if( strlen($talkTag) > 2 ) {
+ if( strlen( $talkTag ) > 2 ) {
$usertalk->doEdit( $talkTag, $reason, EDIT_MINOR );
}
}
diff --git a/includes/specials/SpecialBooksources.php b/includes/specials/SpecialBooksources.php
index db466c14..8ee5467a 100644
--- a/includes/specials/SpecialBooksources.php
+++ b/includes/specials/SpecialBooksources.php
@@ -6,7 +6,7 @@
*
* @author Rob Church <robchur@gmail.com>
* @todo Validate ISBNs using the standard check-digit method
- * @ingroup SpecialPages
+ * @ingroup SpecialPage
*/
class SpecialBookSources extends SpecialPage {
@@ -35,7 +35,7 @@ class SpecialBookSources extends SpecialPage {
$wgOut->addHTML( $this->makeForm() );
if( strlen( $this->isbn ) > 0 ) {
if( !self::isValidISBN( $this->isbn ) ) {
- $wgOut->wrapWikiMsg( '<div class="error">$1</div>', 'booksources-invalid-isbn' );
+ $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1</div>", 'booksources-invalid-isbn' );
}
$this->showList();
}
diff --git a/includes/specials/SpecialBrokenRedirects.php b/includes/specials/SpecialBrokenRedirects.php
index 0a16e6de..b6ae2ada 100644
--- a/includes/specials/SpecialBrokenRedirects.php
+++ b/includes/specials/SpecialBrokenRedirects.php
@@ -33,9 +33,9 @@ class BrokenRedirectsPage extends PageQueryPage {
rd_namespace,
rd_title
FROM $redirect AS rd
- JOIN $page p1 ON (rd.rd_from=p1.page_id)
+ 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
+ WHERE rd_namespace >= 0
AND p2.page_namespace IS NULL";
return $sql;
}
@@ -45,7 +45,7 @@ class BrokenRedirectsPage extends PageQueryPage {
}
function formatResult( $skin, $result ) {
- global $wgUser, $wgContLang;
+ global $wgUser, $wgContLang, $wgLang;
$fromObj = Title::makeTitle( $result->namespace, $result->title );
if ( isset( $result->rd_title ) ) {
@@ -61,21 +61,43 @@ class BrokenRedirectsPage extends PageQueryPage {
// $toObj may very easily be false if the $result list is cached
if ( !is_object( $toObj ) ) {
- return '<s>' . $skin->makeLinkObj( $fromObj ) . '</s>';
+ return '<s>' . $skin->link( $fromObj ) . '</s>';
}
- $from = $skin->makeKnownLinkObj( $fromObj ,'', 'redirect=no' );
- $edit = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-edit' ), 'action=edit' );
- $to = $skin->makeBrokenLinkObj( $toObj );
+ $from = $skin->linkKnown(
+ $fromObj,
+ null,
+ array(),
+ array( 'redirect' => 'no' )
+ );
+ $links = array();
+ $links[] = $skin->linkKnown(
+ $fromObj,
+ wfMsgHtml( 'brokenredirects-edit' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
+ $to = $skin->link(
+ $toObj,
+ null,
+ array(),
+ array(),
+ array( 'broken' )
+ );
$arr = $wgContLang->getArrow();
- $out = "{$from} {$edit}";
+ $out = $from . wfMsg( 'word-separator' );
if( $wgUser->isAllowed( 'delete' ) ) {
- $delete = $skin->makeKnownLinkObj( $fromObj, wfMsgHtml( 'brokenredirects-delete' ), 'action=delete' );
- $out .= " {$delete}";
+ $links[] = $skin->linkKnown(
+ $fromObj,
+ wfMsgHtml( 'brokenredirects-delete' ),
+ array(),
+ array( 'action' => 'delete' )
+ );
}
+ $out .= wfMsg( 'parentheses', $wgLang->pipeList( $links ) );
$out .= " {$arr} {$to}";
return $out;
}
diff --git a/includes/specials/SpecialCategories.php b/includes/specials/SpecialCategories.php
index c6e73f2b..a649eafd 100644
--- a/includes/specials/SpecialCategories.php
+++ b/includes/specials/SpecialCategories.php
@@ -13,9 +13,10 @@ function wfSpecialCategories( $par=null ) {
$from = $par;
}
$cap = new CategoryPager( $from );
+ $cap->doQuery();
$wgOut->addHTML(
XML::openElement( 'div', array('class' => 'mw-spcontent') ) .
- wfMsgExt( 'categoriespagetext', array( 'parse' ) ) .
+ wfMsgExt( 'categoriespagetext', array( 'parse' ), $cap->getNumRows() ) .
$cap->getStartForm( $from ) .
$cap->getNavigationBar() .
'<ul>' . $cap->getBody() . '</ul>' .
@@ -35,10 +36,7 @@ class CategoryPager extends AlphabeticPager {
parent::__construct();
$from = str_replace( ' ', '_', $from );
if( $from !== '' ) {
- global $wgCapitalLinks, $wgContLang;
- if( $wgCapitalLinks ) {
- $from = $wgContLang->ucfirst( $from );
- }
+ $from = Title::capitalize( $from, NS_CATEGORY );
$this->mOffset = $from;
}
}
@@ -74,9 +72,6 @@ class CategoryPager extends AlphabeticPager {
/* 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();
@@ -92,7 +87,7 @@ class CategoryPager extends AlphabeticPager {
function formatRow($result) {
global $wgLang;
$title = Title::makeTitle( NS_CATEGORY, $result->cat_title );
- $titleText = $this->getSkin()->makeLinkObj( $title, htmlspecialchars( $title->getText() ) );
+ $titleText = $this->getSkin()->link( $title, htmlspecialchars( $title->getText() ) );
$count = wfMsgExt( 'nmembers', array( 'parsemag', 'escape' ),
$wgLang->formatNum( $result->cat_pages ) );
return Xml::tags('li', null, "$titleText ($count)" ) . "\n";
diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php
index 9c6f857d..372a574c 100644
--- a/includes/specials/SpecialConfirmemail.php
+++ b/includes/specials/SpecialConfirmemail.php
@@ -34,10 +34,13 @@ class EmailConfirmation extends UnlistedSpecialPage {
}
} else {
$title = SpecialPage::getTitleFor( 'Userlogin' );
- $self = SpecialPage::getTitleFor( 'Confirmemail' );
$skin = $wgUser->getSkin();
- $llink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'loginreqlink' ),
- 'returnto=' . $self->getPrefixedUrl() );
+ $llink = $skin->linkKnown(
+ $title,
+ wfMsgHtml( 'loginreqlink' ),
+ array(),
+ array( 'returnto' => $this->getTitle()->getPrefixedText() )
+ );
$wgOut->addHTML( wfMsgWikiHtml( 'confirmemail_needlogin', $llink ) );
}
} else {
@@ -68,11 +71,10 @@ class EmailConfirmation extends UnlistedSpecialPage {
$wgOut->addWikiMsg( 'emailauthenticated', $time, $d, $t );
}
if( $wgUser->isEmailConfirmationPending() ) {
- $wgOut->wrapWikiMsg( "<div class=\"error mw-confirmemail-pending\">$1</div>", 'confirmemail_pending' );
+ $wgOut->wrapWikiMsg( "<div class=\"error mw-confirmemail-pending\">\n$1</div>", 'confirmemail_pending' );
}
$wgOut->addWikiMsg( 'confirmemail_text' );
- $self = SpecialPage::getTitleFor( 'Confirmemail' );
- $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $self->getLocalUrl() ) );
+ $form = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalUrl() ) );
$form .= Xml::hidden( 'token', $wgUser->editToken() );
$form .= Xml::submitButton( wfMsg( 'confirmemail_send' ) );
$form .= Xml::closeElement( 'form' );
diff --git a/includes/specials/SpecialContributions.php b/includes/specials/SpecialContributions.php
index 9263336e..392f4332 100644
--- a/includes/specials/SpecialContributions.php
+++ b/includes/specials/SpecialContributions.php
@@ -39,7 +39,7 @@ class SpecialContributions extends SpecialPage {
return;
}
- $this->opts['limit'] = $wgRequest->getInt( 'limit', 50 );
+ $this->opts['limit'] = $wgRequest->getInt( 'limit', $wgUser->getOption('rclimit') );
$this->opts['target'] = $target;
$nt = Title::makeTitleSafe( NS_USER, $target );
@@ -89,104 +89,161 @@ class SpecialContributions extends SpecialPage {
return $this->feed( $feedType );
}
- wfRunHooks( 'SpecialContributionsBeforeMainOutput', $id );
+ if ( wfRunHooks( 'SpecialContributionsBeforeMainOutput', array( $id ) ) ) {
- $wgOut->addHTML( $this->getForm() );
+ $wgOut->addHTML( $this->getForm() );
- $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] );
- if( !$pager->getNumRows() ) {
- $wgOut->addWikiMsg( 'nocontribs', $target );
- return;
- }
+ $pager = new ContribsPager( $target, $this->opts['namespace'], $this->opts['year'], $this->opts['month'] );
+ if( !$pager->getNumRows() ) {
+ $wgOut->addWikiMsg( 'nocontribs', $target );
+ } else {
+ # 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>'
+ );
+ }
- # 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>'
- );
+ # Show the appropriate "footer" message - WHOIS tools, etc.
+ if( $target != 'newbies' ) {
+ $message = 'sp-contributions-footer';
+ if ( IP::isIPAddress( $target ) ) {
+ $message = 'sp-contributions-footer-anon';
+ } else {
+ $user = User::newFromName( $target );
+ if ( !$user || $user->isAnon() ) {
+ // No message for non-existing users
+ return;
+ }
+ }
- # 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>' );
+ $text = wfMsgNoTrans( $message, $target );
+ if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
+ $wgOut->wrapWikiMsg(
+ "<div class='mw-contributions-footer'>\n$1\n</div>",
+ array( $message, $target ) );
+ }
}
}
}
protected function setSyndicated() {
global $wgOut;
- $queryParams = array(
- 'namespace' => $this->opts['namespace'],
- 'target' => $this->opts['target']
- );
$wgOut->setSyndicated( true );
- $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
+ $wgOut->setFeedAppendQuery( wfArrayToCGI( $this->opts ) );
}
/**
- * 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
- */
+ * Generates the subheading with links
+ * @param Title $nt @see Title object for the target
+ * @param integer $id User ID for the target
+ * @return String: appropriately-escaped HTML to be output literally
+ * @todo Fixme: almost the same as getSubTitle in SpecialDeletedContributions.php. Could be combined.
+ */
protected function contributionsSub( $nt, $id ) {
- global $wgSysopUserBans, $wgLang, $wgUser;
+ global $wgSysopUserBans, $wgLang, $wgUser, $wgOut;
$sk = $wgUser->getSkin();
- if( 0 == $id ) {
- $user = $nt->getText();
+ if ( $id === null ) {
+ $user = htmlspecialchars( $nt->getText() );
} else {
- $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+ $user = $sk->link( $nt, htmlspecialchars( $nt->getText() ) );
}
+ $userObj = User::newFromName( $nt->getText(), /* check for username validity not needed */ false );
$talk = $nt->getTalkPage();
if( $talk ) {
# Talk page link
- $tools[] = $sk->makeLinkObj( $talk, wfMsgHtml( 'talkpagelinktext' ) );
- if( ( $id != 0 && $wgSysopUserBans ) || ( $id == 0 && IP::isIPAddress( $nt->getText() ) ) ) {
- # Block link
- if( $wgUser->isAllowed( 'block' ) )
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Blockip',
- $nt->getDBkey() ), wfMsgHtml( 'blocklink' ) );
+ $tools[] = $sk->link( $talk, wfMsgHtml( 'sp-contributions-talk' ) );
+ if( ( $id !== null && $wgSysopUserBans ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) {
+ if( $wgUser->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
+ if ( $userObj->isBlocked() ) {
+ $tools[] = $sk->linkKnown( # Change block link
+ SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ),
+ wfMsgHtml( 'change-blocklink' )
+ );
+ $tools[] = $sk->linkKnown( # Unblock link
+ SpecialPage::getTitleFor( 'BlockList' ),
+ wfMsgHtml( 'unblocklink' ),
+ array(),
+ array(
+ 'action' => 'unblock',
+ 'ip' => $nt->getDBkey()
+ )
+ );
+ }
+ else { # User is not blocked
+ $tools[] = $sk->linkKnown( # Block link
+ 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() );
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'sp-contributions-blocklog' ),
+ array(),
+ array(
+ 'type' => 'block',
+ 'page' => $nt->getPrefixedText()
+ )
+ );
}
# Other logs link
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ), wfMsg( 'sp-contributions-logs' ),
- 'user=' . $nt->getPartialUrl() );
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'sp-contributions-logs' ),
+ array(),
+ array( 'user' => $nt->getText() )
+ );
# Add link to deleted user contributions for priviledged users
if( $wgUser->isAllowed( 'deletedhistory' ) ) {
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'DeletedContributions',
- $nt->getDBkey() ), wfMsgHtml( 'deletedcontributions' ) );
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'DeletedContributions', $nt->getDBkey() ),
+ wfMsgHtml( 'sp-contributions-deleted' )
+ );
}
# Add a link to change user rights for privileged users
$userrightsPage = new UserrightsPage();
- if( 0 !== $id && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) {
- $tools[] = $sk->makeKnownLinkObj(
+ if( $id !== null && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) {
+ $tools[] = $sk->linkKnown(
SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ),
- wfMsgHtml( 'userrights' )
+ wfMsgHtml( 'sp-contributions-userrights' )
);
}
wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
-
+
$links = $wgLang->pipeList( $tools );
+
+ // Show a note if the user is blocked and display the last block log entry.
+ if ( $userObj->isBlocked() ) {
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'block',
+ $nt->getPrefixedText(),
+ '',
+ array(
+ 'lim' => 1,
+ 'showIfEmpty' => false,
+ 'msgKey' => array(
+ 'sp-contributions-blocked-notice',
+ $nt->getText() # Support GENDER in 'sp-contributions-blocked-notice'
+ ),
+ 'offset' => '' # don't use $wgRequest parameter offset
+ )
+ );
+ }
}
-
+
// 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,
@@ -203,9 +260,9 @@ class SpecialContributions extends SpecialPage {
* @param $this->opts Array: the options to be included.
*/
protected function getForm() {
- global $wgScript, $wgTitle;
+ global $wgScript;
- $this->opts['title'] = $wgTitle->getPrefixedText();
+ $this->opts['title'] = $this->getTitle()->getPrefixedText();
if( !isset( $this->opts['target'] ) ) {
$this->opts['target'] = '';
} else {
@@ -249,11 +306,14 @@ class SpecialContributions extends SpecialPage {
$f .= '<fieldset>' .
Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
- Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parseinline' ) ),
+ Xml::radioLabel( wfMsgExt( 'sp-contributions-newbies', array( 'parsemag' ) ),
'contribs', 'newbie' , 'newbie', $this->opts['contribs'] == 'newbie' ? true : false ) . '<br />' .
- Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parseinline' ) ),
+ Xml::radioLabel( wfMsgExt( 'sp-contributions-username', array( 'parsemag' ) ),
'contribs' , 'user', 'user', $this->opts['contribs'] == 'user' ? true : false ) . ' ' .
- Xml::input( 'target', 20, $this->opts['target']) . ' '.
+ Html::input( 'target', $this->opts['target'], 'text', array(
+ 'size' => '20',
+ 'required' => ''
+ ) + ( $this->opts['target'] ? array() : array( 'autofocus' ) ) ) . ' '.
'<span style="white-space: nowrap">' .
Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
Xml::namespaceSelector( $this->opts['namespace'], '' ) .
@@ -268,7 +328,7 @@ class SpecialContributions extends SpecialPage {
$explain = wfMsgExt( 'sp-contributions-explain', 'parseinline' );
if( !wfEmptyMsg( 'sp-contributions-explain', $explain ) )
- $f .= "<p>{$explain}</p>";
+ $f .= "<p id='mw-sp-contributions-explain'>{$explain}</p>";
$f .= '</fieldset>' .
Xml::closeElement( 'form' );
@@ -341,7 +401,7 @@ class SpecialContributions extends SpecialPage {
$comments
);
} else {
- return NULL;
+ return null;
}
}
@@ -371,9 +431,13 @@ class ContribsPager extends ReverseChronologicalPager {
function __construct( $target, $namespace = false, $year = false, $month = false, $tagFilter = false ) {
parent::__construct();
- foreach( explode( ' ', 'uctop diff newarticle rollbacklink diff hist newpageletter minoreditletter' ) as $msg ) {
- $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+
+ $msgs = array( 'uctop', 'diff', 'newarticle', 'rollbacklink', 'diff', 'hist', 'rev-delundel', 'pipe-separator' );
+
+ foreach( $msgs as $msg ) {
+ $this->messages[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) );
}
+
$this->target = $target;
$this->namespace = $namespace;
$this->tagFilter = $tagFilter;
@@ -395,8 +459,11 @@ class ContribsPager extends ReverseChronologicalPager {
$conds = array_merge( $userCond, $this->getNamespaceCond() );
// Paranoia: avoid brute force searches (bug 17342)
- if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
- $conds[] = 'rev_deleted & ' . Revision::DELETED_USER . ' = 0';
+ if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
+ $conds[] = $this->mDb->bitAnd('rev_deleted',Revision::DELETED_USER) . ' = 0';
+ } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
+ $conds[] = $this->mDb->bitAnd('rev_deleted',Revision::SUPPRESSED_USER) .
+ ' != ' . Revision::SUPPRESSED_USER;
}
$join_cond['page'] = array( 'INNER JOIN', 'page_id=rev_page' );
@@ -411,14 +478,16 @@ class ContribsPager extends ReverseChronologicalPager {
'options' => array( 'USE INDEX' => array('revision' => $index) ),
'join_conds' => $join_cond
);
-
- ChangeTags::modifyDisplayQuery( $queryInfo['tables'],
- $queryInfo['fields'],
- $queryInfo['conds'],
- $queryInfo['join_conds'],
- $queryInfo['options'],
- $this->tagFilter );
-
+
+ ChangeTags::modifyDisplayQuery(
+ $queryInfo['tables'],
+ $queryInfo['fields'],
+ $queryInfo['conds'],
+ $queryInfo['join_conds'],
+ $queryInfo['options'],
+ $this->tagFilter
+ );
+
wfRunHooks( 'ContribsPager::getQueryInfo', array( &$this, &$queryInfo ) );
return $queryInfo;
}
@@ -473,7 +542,7 @@ class ContribsPager extends ReverseChronologicalPager {
* @todo This would probably look a lot nicer in a table.
*/
function formatRow( $row ) {
- global $wgLang, $wgUser, $wgContLang;
+ global $wgUser, $wgLang, $wgContLang;
wfProfileIn( __METHOD__ );
$sk = $this->getSkin();
@@ -482,60 +551,101 @@ class ContribsPager extends ReverseChronologicalPager {
$page = Title::newFromRow( $row );
$page->resetArticleId( $row->rev_page ); // use process cache
- $link = $sk->makeLinkObj( $page, $page->getPrefixedText(), $page->isRedirect() ? 'redirect=no' : '' );
+ $link = $sk->link(
+ $page,
+ htmlspecialchars( $page->getPrefixedText() ),
+ array(),
+ $page->isRedirect() ? array( 'redirect' => 'no' ) : array()
+ );
# Mark current revisions
$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' ) . ')';
- # Add rollback link
- if( $page->quickUserCan( 'rollback') && $page->quickUserCan( 'edit' ) ) {
- $topmarktext .= ' '.$sk->generateRollback( $rev );
- }
- } else {
- $difftext .= $this->messages['newarticle'];
+ $topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
+ # Add rollback link
+ if( !$row->page_is_new && $page->quickUserCan( 'rollback' )
+ && $page->quickUserCan( 'edit' ) )
+ {
+ $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 ) . ')';
+ if( $rev->userCan( Revision::DELETED_TEXT ) && $rev->getParentId() !== 0 ) {
+ $difftext = $sk->linkKnown(
+ $page,
+ $this->messages['diff'],
+ array(),
+ array(
+ 'diff' => 'prev',
+ 'oldid' => $row->rev_id
+ )
+ );
} else {
- $difftext = '(' . $this->messages['diff'] . ')';
+ $difftext = $this->messages['diff'];
}
- $histlink = '('.$sk->makeKnownLinkObj( $page, $this->messages['hist'], 'action=history' ) . ')';
+ $histlink = $sk->linkKnown(
+ $page,
+ $this->messages['hist'],
+ array(),
+ array( 'action' => 'history' )
+ );
$comment = $wgContLang->getDirMark() . $sk->revComment( $rev, false, true );
$date = $wgLang->timeanddate( wfTimestamp( TS_MW, $row->rev_timestamp ), true );
- $d = $sk->makeKnownLinkObj( $page, $date, 'oldid='.intval($row->rev_id) );
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $d = '<span class="history-deleted">' . $date . '</span>';
+ } else {
+ $d = $sk->linkKnown(
+ $page,
+ htmlspecialchars($date),
+ array(),
+ array( 'oldid' => intval( $row->rev_id ) )
+ );
+ }
if( $this->target == 'newbies' ) {
$userlink = ' . . ' . $sk->userLink( $row->rev_user, $row->rev_user_text );
- $userlink .= ' (' . $sk->userTalkLink( $row->rev_user, $row->rev_user_text ) . ') ';
+ $userlink .= ' ' . wfMsg( 'parentheses', $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>';
+ $nflag = ChangesList::flag( 'newpage' );
} else {
$nflag = '';
}
if( $rev->isMinor() ) {
- $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
+ $mflag = ChangesList::flag( 'minor' );
} else {
$mflag = '';
}
- $ret = "{$d} {$histlink} {$difftext} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}";
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+ // Don't show useless link to people who cannot hide revisions
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) {
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $del = $this->mSkin->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ } else {
+ $query = array(
+ 'type' => 'revision',
+ 'target' => $page->getPrefixedDbkey(),
+ 'ids' => $rev->getId()
+ );
+ $del = $this->mSkin->revDeleteLink( $query,
+ $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
+ }
+ $del .= ' ';
+ } else {
+ $del = '';
+ }
+
+ $diffHistLinks = '(' . $difftext . $this->messages['pipe-separator'] . $histlink . ')';
+ $ret = "{$del}{$d} {$diffHistLinks} {$nflag}{$mflag} {$link}{$userlink} {$comment} {$topmarktext}";
+
+ # Denote if username is redacted for this edit
+ if( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ $ret .= " <strong>" . wfMsgHtml('rev-deleted-user-contribs') . "</strong>";
}
# Tags, if any.
diff --git a/includes/specials/SpecialDeletedContributions.php b/includes/specials/SpecialDeletedContributions.php
index 67b05ca1..8884bb22 100644
--- a/includes/specials/SpecialDeletedContributions.php
+++ b/includes/specials/SpecialDeletedContributions.php
@@ -11,8 +11,9 @@ class DeletedContribsPager extends IndexPager {
function __construct( $target, $namespace = false ) {
parent::__construct();
- foreach( explode( ' ', 'deletionlog undeletebtn minoreditletter diff' ) as $msg ) {
- $this->messages[$msg] = wfMsgExt( $msg, array( 'escape') );
+ $msgs = array( 'deletionlog', 'undeleteviewlink', 'diff' );
+ foreach( $msgs as $msg ) {
+ $this->messages[$msg] = wfMsgExt( $msg, array( 'escapenoentities') );
}
$this->target = $target;
$this->namespace = $namespace;
@@ -30,8 +31,11 @@ class DeletedContribsPager extends IndexPager {
list( $index, $userCond ) = $this->getUserCond();
$conds = array_merge( $userCond, $this->getNamespaceCond() );
// Paranoia: avoid brute force searches (bug 17792)
- if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
- $conds[] = 'ar_deleted & ' . Revision::DELETED_USER . ' = 0';
+ if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
+ $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::DELETED_USER) . ' = 0';
+ } else if( !$wgUser->isAllowed( 'suppressrevision' ) ) {
+ $conds[] = $this->mDb->bitAnd('ar_deleted',Revision::SUPPRESSED_USER) .
+ ' != ' . Revision::SUPPRESSED_USER;
}
return array(
'tables' => array( 'archive' ),
@@ -71,9 +75,10 @@ class DeletedContribsPager extends IndexPager {
if ( isset( $this->mNavigationBar ) ) {
return $this->mNavigationBar;
}
+ $fmtLimit = $wgLang->formatNum( $this->mLimit );
$linkTexts = array(
- 'prev' => wfMsgHtml( 'pager-newer-n', $this->mLimit ),
- 'next' => wfMsgHtml( 'pager-older-n', $this->mLimit ),
+ 'prev' => wfMsgExt( 'pager-newer-n', array( 'escape', 'parsemag' ), $fmtLimit ),
+ 'next' => wfMsgExt( 'pager-older-n', array( 'escape', 'parsemag' ), $fmtLimit ),
'first' => wfMsgHtml( 'histlast' ),
'last' => wfMsgHtml( 'histfirst' )
);
@@ -83,7 +88,7 @@ class DeletedContribsPager extends IndexPager {
$limits = $wgLang->pipeList( $limitLinks );
$this->mNavigationBar = "(" . $wgLang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " .
- wfMsgExt( 'viewprevnext', array( 'parsemag' ), $pagingLinks['prev'], $pagingLinks['next'], $limits );
+ wfMsgExt( 'viewprevnext', array( 'parsemag', 'escape', 'replaceafter' ), $pagingLinks['prev'], $pagingLinks['next'], $limits );
return $this->mNavigationBar;
}
@@ -106,10 +111,9 @@ class DeletedContribsPager extends IndexPager {
* @todo This would probably look a lot nicer in a table.
*/
function formatRow( $row ) {
+ global $wgUser, $wgLang;
wfProfileIn( __METHOD__ );
- global $wgLang, $wgUser;
-
$sk = $this->getSkin();
$rev = new Revision( array(
@@ -119,7 +123,7 @@ class DeletedContribsPager extends IndexPager {
'user_text' => $row->ar_user_text,
'timestamp' => $row->ar_timestamp,
'minor_edit' => $row->ar_minor_edit,
- 'deleted' => $row->ar_deleted,
+ 'deleted' => $row->ar_deleted,
) );
$page = Title::makeTitle( $row->ar_namespace, $row->ar_title );
@@ -127,50 +131,96 @@ class DeletedContribsPager extends IndexPager {
$undelete = SpecialPage::getTitleFor( 'Undelete' );
$logs = SpecialPage::getTitleFor( 'Log' );
- $dellog = $sk->makeKnownLinkObj( $logs,
+ $dellog = $sk->linkKnown(
+ $logs,
$this->messages['deletionlog'],
- 'type=delete&page=' . $page->getPrefixedUrl() );
-
- $reviewlink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ),
- $this->messages['undeletebtn'] );
+ array(),
+ array(
+ 'type' => 'delete',
+ 'page' => $page->getPrefixedText()
+ )
+ );
- $link = $sk->makeKnownLinkObj( $undelete,
- htmlspecialchars( $page->getPrefixedText() ),
- 'target=' . $page->getPrefixedUrl() .
- '&timestamp=' . $rev->getTimestamp() );
+ $reviewlink = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ),
+ $this->messages['undeleteviewlink']
+ );
- $last = $sk->makeKnownLinkObj( $undelete,
- $this->messages['diff'],
- "target=" . $page->getPrefixedUrl() .
- "&timestamp=" . $rev->getTimestamp() .
- "&diff=prev" );
+ if( $wgUser->isAllowed('deletedtext') ) {
+ $last = $sk->linkKnown(
+ $undelete,
+ $this->messages['diff'],
+ array(),
+ array(
+ 'target' => $page->getPrefixedText(),
+ 'timestamp' => $rev->getTimestamp(),
+ 'diff' => 'prev'
+ )
+ );
+ } else {
+ $last = $this->messages['diff'];
+ }
$comment = $sk->revComment( $rev );
- $d = $wgLang->timeanddate( $rev->getTimestamp(), true );
+ $date = htmlspecialchars( $wgLang->timeanddate( $rev->getTimestamp(), true ) );
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $d = '<span class="history-deleted">' . $d . '</span>';
+ if( !$wgUser->isAllowed('undelete') || !$rev->userCan(Revision::DELETED_TEXT) ) {
+ $link = $date; // unusable link
} else {
- $link = $sk->makeKnownLinkObj( $undelete, $d,
- 'target=' . $page->getPrefixedUrl() .
- '&timestamp=' . $rev->getTimestamp() );
+ $link = $sk->linkKnown(
+ $undelete,
+ $date,
+ array(),
+ array(
+ 'target' => $page->getPrefixedText(),
+ 'timestamp' => $rev->getTimestamp()
+ )
+ );
+ }
+ // Style deleted items
+ if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
+ $link = '<span class="history-deleted">' . $link . '</span>';
}
- $pagelink = $sk->makeLinkObj( $page );
+ $pagelink = $sk->link( $page );
if( $rev->isMinor() ) {
- $mflag = '<span class="minor">' . $this->messages['minoreditletter'] . '</span> ';
+ $mflag = ChangesList::flag( 'minor' );
} else {
$mflag = '';
}
+
+ // Revision delete link
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) {
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $del = $this->mSkin->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ } else {
+ $query = array(
+ 'type' => 'archive',
+ 'target' => $page->getPrefixedDbkey(),
+ 'ids' => $rev->getTimestamp() );
+ $del = $this->mSkin->revDeleteLink( $query,
+ $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide ) . ' ';
+ }
+ } else {
+ $del = '';
+ }
-
- $ret = "{$link} ($last) ({$dellog}) ({$reviewlink}) . . {$mflag} {$pagelink} {$comment}";
- if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
- $ret .= ' ' . wfMsgHtml( 'deletedrev' );
+ $tools = Html::rawElement(
+ 'span',
+ array( 'class' => 'mw-deletedcontribs-tools' ),
+ wfMsg( 'parentheses', $wgLang->pipeList( array( $last, $dellog, $reviewlink ) ) )
+ );
+
+ $ret = "{$del}{$link} {$tools} . . {$mflag} {$pagelink} {$comment}";
+
+ # Denote if username is redacted for this edit
+ if( $rev->isDeleted( Revision::DELETED_USER ) ) {
+ $ret .= " <strong>" . wfMsgHtml('rev-deleted-user-contribs') . "</strong>";
}
- $ret = "<li>$ret</li>\n";
+ $ret = Html::rawElement( 'li', array(), $ret ) . "\n";
wfProfileOut( __METHOD__ );
return $ret;
@@ -208,7 +258,7 @@ class DeletedContributionsPage extends SpecialPage {
return;
}
- global $wgUser, $wgOut, $wgLang, $wgRequest;
+ global $wgOut, $wgLang, $wgRequest;
$wgOut->setPageTitle( wfMsgExt( 'deletedcontributions-title', array( 'parsemag' ) ) );
@@ -248,7 +298,7 @@ class DeletedContributionsPage extends SpecialPage {
$pager = new DeletedContribsPager( $target, $options['namespace'] );
if ( !$pager->getNumRows() ) {
- $wgOut->addWikiText( wfMsg( 'nocontribs' ) );
+ $wgOut->addWikiMsg( 'nocontribs' );
return;
}
@@ -271,50 +321,112 @@ class DeletedContributionsPage extends SpecialPage {
$text = wfMsgNoTrans( $message, $target );
if( !wfEmptyMsg( $message, $text ) && $text != '-' ) {
- $wgOut->addHTML( '<div class="mw-contributions-footer">' );
- $wgOut->addWikiText( $text );
- $wgOut->addHTML( '</div>' );
+ $wgOut->wrapWikiMsg( "<div class='mw-contributions-footer'>\n$1\n</div>", array( $message, $target ) );
}
}
}
/**
* Generates the subheading with links
- * @param $nt @see Title object for the target
+ * @param Title $nt @see Title object for the target
+ * @param integer $id User ID for the target
+ * @return String: appropriately-escaped HTML to be output literally
+ * @todo Fixme: almost the same as contributionsSub in SpecialContributions.php. Could be combined.
*/
function getSubTitle( $nt, $id ) {
- global $wgSysopUserBans, $wgLang, $wgUser;
+ global $wgSysopUserBans, $wgLang, $wgUser, $wgOut;
$sk = $wgUser->getSkin();
- if ( 0 == $id ) {
- $user = $nt->getText();
+ if ( $id === null ) {
+ $user = htmlspecialchars( $nt->getText() );
} else {
- $user = $sk->makeLinkObj( $nt, htmlspecialchars( $nt->getText() ) );
+ $user = $sk->link( $nt, htmlspecialchars( $nt->getText() ) );
}
+ $userObj = User::newFromName( $nt->getText(), /* check for username validity not needed */ false );
$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' ) );
+ $tools[] = $sk->link( $talk, wfMsgHtml( 'sp-contributions-talk' ) );
+ if( ( $id !== null && $wgSysopUserBans ) || ( $id === null && IP::isIPAddress( $nt->getText() ) ) ) {
+ if( $wgUser->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
+ if ( $userObj->isBlocked() ) {
+ $tools[] = $sk->linkKnown( # Change block link
+ SpecialPage::getTitleFor( 'Blockip', $nt->getDBkey() ),
+ wfMsgHtml( 'change-blocklink' )
+ );
+ $tools[] = $sk->linkKnown( # Unblock link
+ SpecialPage::getTitleFor( 'BlockList' ),
+ wfMsgHtml( 'unblocklink' ),
+ array(),
+ array(
+ 'action' => 'unblock',
+ 'ip' => $nt->getDBkey()
+ )
+ );
+ }
+ else { # User is not blocked
+ $tools[] = $sk->linkKnown( # Block link
+ 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() );
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'sp-contributions-blocklog' ),
+ array(),
+ array(
+ 'type' => 'block',
+ 'page' => $nt->getPrefixedText()
+ )
+ );
}
# Other logs link
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Log' ),
- wfMsgHtml( 'log' ), 'user=' . $nt->getPartialUrl() );
- # Link to undeleted contributions
- $tools[] = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ),
- wfMsgHtml( 'contributions' ) );
-
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'sp-contributions-logs' ),
+ array(),
+ array( 'user' => $nt->getText() )
+ );
+ # Link to contributions
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Contributions', $nt->getDBkey() ),
+ wfMsgHtml( 'sp-deletedcontributions-contribs' )
+ );
+
+ # Add a link to change user rights for privileged users
+ $userrightsPage = new UserrightsPage();
+ if( $id !== null && $userrightsPage->userCanChangeRights( User::newFromId( $id ) ) ) {
+ $tools[] = $sk->linkKnown(
+ SpecialPage::getTitleFor( 'Userrights', $nt->getDBkey() ),
+ wfMsgHtml( 'sp-contributions-userrights' )
+ );
+ }
+
wfRunHooks( 'ContributionsToolLinks', array( $id, $nt, &$tools ) );
$links = $wgLang->pipeList( $tools );
+
+ // Show a note if the user is blocked and display the last block log entry.
+ if ( $userObj->isBlocked() ) {
+ LogEventsList::showLogExtract(
+ $wgOut,
+ 'block',
+ $nt->getPrefixedText(),
+ '',
+ array(
+ 'lim' => 1,
+ 'showIfEmpty' => false,
+ 'msgKey' => array(
+ 'sp-contributions-blocked-notice',
+ $nt->getText() # Support GENDER in 'sp-contributions-blocked-notice'
+ ),
+ 'offset' => '' # don't use $wgRequest parameter offset
+ )
+ );
+ }
}
// Old message 'contribsub' had one parameter, but that doesn't work for
@@ -333,9 +445,9 @@ class DeletedContributionsPage extends SpecialPage {
* @param $options Array: the options to be included.
*/
function getForm( $options ) {
- global $wgScript, $wgTitle, $wgRequest;
+ global $wgScript, $wgRequest;
- $options['title'] = $wgTitle->getPrefixedText();
+ $options['title'] = SpecialPage::getTitleFor( 'DeletedContributions' )->getPrefixedText();
if ( !isset( $options['target'] ) ) {
$options['target'] = '';
} else {
@@ -366,7 +478,10 @@ class DeletedContributionsPage extends SpecialPage {
$f .= Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), wfMsg( 'sp-contributions-search' ) ) .
Xml::tags( 'label', array( 'for' => 'target' ), wfMsgExt( 'sp-contributions-username', 'parseinline' ) ) . ' ' .
- Xml::input( 'target', 20, $options['target']) . ' '.
+ Html::input( 'target', $options['target'], 'text', array(
+ 'size' => '20',
+ 'required' => ''
+ ) + ( $options['target'] ? array() : array( 'autofocus' ) ) ) . ' '.
Xml::label( wfMsg( 'namespace' ), 'namespace' ) . ' ' .
Xml::namespaceSelector( $options['namespace'], '' ) . ' ' .
Xml::submitButton( wfMsg( 'sp-contributions-submit' ) ) .
diff --git a/includes/specials/SpecialDisambiguations.php b/includes/specials/SpecialDisambiguations.php
index 0a728b68..1941112a 100644
--- a/includes/specials/SpecialDisambiguations.php
+++ b/includes/specials/SpecialDisambiguations.php
@@ -88,7 +88,7 @@ class DisambiguationsPage extends PageQueryPage {
$dp = Title::makeTitle( $result->namespace, $result->title );
$from = $skin->link( $title );
- $edit = $skin->link( $title, "(".wfMsgHtml("qbedit").")", array(), array( 'redirect' => 'no', 'action' => 'edit' ) );
+ $edit = $skin->link( $title, wfMsgExt( 'parentheses', array( 'escape' ), wfMsg( 'editlink' ) ) , array(), array( 'redirect' => 'no', 'action' => 'edit' ) );
$arr = $wgContLang->getArrow();
$to = $skin->link( $dp );
diff --git a/includes/specials/SpecialDoubleRedirects.php b/includes/specials/SpecialDoubleRedirects.php
index b1bad0c3..893fee9e 100644
--- a/includes/specials/SpecialDoubleRedirects.php
+++ b/includes/specials/SpecialDoubleRedirects.php
@@ -74,16 +74,34 @@ class DoubleRedirectsPage extends PageQueryPage {
}
}
if ( !$result ) {
- return '<s>' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . '</s>';
+ return '<s>' . $skin->link( $titleA, null, array(), array( '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 );
+ $linkA = $skin->linkKnown(
+ $titleA,
+ null,
+ array(),
+ array( 'redirect' => 'no' )
+ );
+ $edit = $skin->linkKnown(
+ $titleA,
+ wfMsgExt( 'parentheses', array( 'escape' ), wfMsg( 'editlink' ) ),
+ array(),
+ array(
+ 'redirect' => 'no',
+ 'action' => 'edit'
+ )
+ );
+ $linkB = $skin->linkKnown(
+ $titleB,
+ null,
+ array(),
+ array( 'redirect' => 'no' )
+ );
+ $linkC = $skin->linkKnown( $titleC );
$arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
return( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php
index 58e2514e..48088ded 100644
--- a/includes/specials/SpecialEmailuser.php
+++ b/includes/specials/SpecialEmailuser.php
@@ -48,6 +48,12 @@ function wfSpecialEmailuser( $par ) {
case 'mailnologin':
$wgOut->showErrorPage( 'mailnologin', 'mailnologintext' );
return;
+ default:
+ // It's a hook error
+ list( $title, $msg, $params ) = $error;
+ $wgOut->showErrorPage( $title, $msg, $params );
+ return;
+
}
}
@@ -256,7 +262,7 @@ class EmailUserForm {
}
static function validateEmailTarget ( $target ) {
- if ( "" == $target ) {
+ if ( $target == "" ) {
wfDebug( "Target is empty.\n" );
return "notarget";
}
@@ -268,7 +274,7 @@ class EmailUserForm {
}
$nu = User::newFromName( $nt->getText() );
- if( is_null( $nu ) || !$nu->getId() ) {
+ if( !$nu instanceof User || !$nu->getId() ) {
wfDebug( "Target is invalid user.\n" );
return "notarget";
} else if ( !$nu->isEmailConfirmed() ) {
@@ -284,6 +290,10 @@ class EmailUserForm {
static function getPermissionsError ( $user, $editToken ) {
if( !$user->canSendEmail() ) {
wfDebug( "User can't send.\n" );
+ // FIXME: this is also the error if user is in a group
+ // that is not allowed to send e-mail (no right
+ // 'sendemail'). Error messages should probably
+ // be more fine grained.
return "mailnologin";
}
@@ -297,12 +307,17 @@ class EmailUserForm {
return 'actionthrottledtext';
}
+ $hookErr = null;
+ wfRunHooks( 'EmailUserPermissionsErrors', array( $user, $editToken, &$hookErr ) );
+
+ if ($hookErr) {
+ return $hookErr;
+ }
+
if( !$user->matchEditToken( $editToken ) ) {
wfDebug( "Matching edit token failed.\n" );
return 'sessionfailure';
}
-
- return;
}
static function newFromURL( $target, $text, $subject, $cc_me )
diff --git a/includes/specials/SpecialExport.php b/includes/specials/SpecialExport.php
index 8bf16a71..b9a44d48 100644
--- a/includes/specials/SpecialExport.php
+++ b/includes/specials/SpecialExport.php
@@ -44,17 +44,18 @@ class SpecialExport extends SpecialPage {
$this->templates = $wgRequest->getCheck( 'templates' );
$this->images = $wgRequest->getCheck( 'images' ); // Doesn't do anything yet
$this->pageLinkDepth = $this->validateLinkDepth(
- $wgRequest->getIntOrNull( 'pagelink-depth' ) );
+ $wgRequest->getIntOrNull( 'pagelink-depth' ) );
+ $nsindex = '';
if ( $wgRequest->getCheck( 'addcat' ) ) {
$page = $wgRequest->getText( 'pages' );
$catname = $wgRequest->getText( 'catname' );
- if ( $catname !== '' && $catname !== NULL && $catname !== false ) {
+ if ( $catname !== '' && $catname !== null && $catname !== false ) {
$t = Title::makeTitleSafe( NS_MAIN, $catname );
if ( $t ) {
/**
- * @fixme This can lead to hitting memory limit for very large
+ * @todo 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.
*/
@@ -65,15 +66,15 @@ class SpecialExport extends SpecialPage {
}
else if( $wgRequest->getCheck( 'addns' ) && $wgExportFromNamespaces ) {
$page = $wgRequest->getText( 'pages' );
- $nsindex = $wgRequest->getText( 'nsindex' );
+ $nsindex = $wgRequest->getText( 'nsindex', '' );
- if ( $nsindex !== '' && $nsindex !== NULL && $nsindex !== false ) {
+ if ( strval( $nsindex ) !== '' ) {
/**
- * Same implementation as above, so same @fixme
+ * Same implementation as above, so same @todo
*/
$nspages = $this->getPagesFromNamespace( $nsindex );
if ( $nspages ) $page .= "\n" . implode( "\n", $nspages );
- }
+ }
}
else if( $wgRequest->wasPosted() && $par == '' ) {
$page = $wgRequest->getText( 'pages' );
@@ -87,15 +88,15 @@ class SpecialExport extends SpecialPage {
$limit = $wgRequest->getInt( 'limit' );
$dir = $wgRequest->getVal( 'dir' );
$history = array(
- 'dir' => 'asc',
- 'offset' => false,
- 'limit' => $wgExportMaxHistory,
- );
+ 'dir' => 'asc',
+ 'offset' => false,
+ 'limit' => $wgExportMaxHistory,
+ );
$historyCheck = $wgRequest->getCheck( 'history' );
if ( $this->curonly ) {
$history = WikiExporter::CURRENT;
} elseif ( !$historyCheck ) {
- if ( $limit > 0 && $limit < $wgExportMaxHistory ) {
+ if ( $limit > 0 && ($wgExportMaxHistory == 0 || $limit < $wgExportMaxHistory ) ) {
$history['limit'] = $limit;
}
if ( !is_null( $offset ) ) {
@@ -146,12 +147,12 @@ class SpecialExport extends SpecialPage {
$wgOut->addWikiMsg( 'exporttext' );
$form = Xml::openElement( 'form', array( 'method' => 'post',
- 'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) );
+ 'action' => $this->getTitle()->getLocalUrl( 'action=submit' ) ) );
$form .= Xml::inputLabel( wfMsg( 'export-addcattext' ) , 'catname', 'catname', 40 ) . '&nbsp;';
$form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '<br />';
if ( $wgExportFromNamespaces ) {
- $form .= Xml::namespaceSelector( '', null, 'nsindex', wfMsg( 'export-addnstext' ) ) . '&nbsp;';
+ $form .= Xml::namespaceSelector( $nsindex, null, 'nsindex', wfMsg( 'export-addnstext' ) ) . '&nbsp;';
$form .= Xml::submitButton( wfMsg( 'export-addns' ), array( 'name' => 'addns' ) ) . '<br />';
}
@@ -190,10 +191,22 @@ class SpecialExport extends SpecialPage {
private function doExport( $page, $history, $list_authors ) {
global $wgExportMaxHistory;
- /* Split up the input and look up linked pages */
- $inputPages = array_filter( explode( "\n", $page ), array( $this, 'filterPage' ) );
- $pageSet = array_flip( $inputPages );
+ $pageSet = array(); // Inverted index of all pages to look up
+
+ // Split up and normalize input
+ foreach( explode( "\n", $page ) as $pageName ) {
+ $pageName = trim( $pageName );
+ $title = Title::newFromText( $pageName );
+ if( $title && $title->getInterwiki() == '' && $title->getText() !== '' ) {
+ // Only record each page once!
+ $pageSet[$title->getPrefixedText()] = true;
+ }
+ }
+ // Set of original pages to pass on to further manipulation...
+ $inputPages = array_keys( $pageSet );
+
+ // Look up any linked pages if asked...
if( $this->templates ) {
$pageSet = $this->getTemplates( $inputPages, $pageSet );
}
@@ -210,7 +223,13 @@ class SpecialExport extends SpecialPage {
*/
$pages = array_keys( $pageSet );
-
+
+ // Normalize titles to the same format and remove dupes, see bug 17374
+ foreach( $pages as $k => $v ) {
+ $pages[$k] = str_replace( " ", "_", $v );
+ }
+ $pages = array_unique( $pages );
+
/* Ok, let's get to it... */
if( $history == WikiExporter::CURRENT ) {
$lb = false;
@@ -256,8 +275,7 @@ class SpecialExport extends SpecialPage {
$lb->closeAll();
}
}
-
-
+
private function getPagesFromCategory( $title ) {
global $wgContLang;
@@ -374,7 +392,7 @@ class SpecialExport extends SpecialPage {
$title = Title::newFromText( $page );
if( $title ) {
$pageSet[$title->getPrefixedText()] = true;
- /// @fixme May or may not be more efficient to batch these
+ /// @todo Fixme: May or may not be more efficient to batch these
/// by namespace when given multiple input pages.
$result = $dbr->select(
array( 'page', $table ),
@@ -382,7 +400,7 @@ class SpecialExport extends SpecialPage {
array_merge( $join,
array(
'page_namespace' => $title->getNamespace(),
- 'page_title' => $title->getDBKey() ) ),
+ 'page_title' => $title->getDBkey() ) ),
__METHOD__ );
foreach( $result as $row ) {
$template = Title::makeTitle( $row->namespace, $row->title );
@@ -392,12 +410,5 @@ class SpecialExport extends SpecialPage {
}
return $pageSet;
}
-
- /**
- * Callback function to remove empty strings from the pages array.
- */
- private function filterPage( $page ) {
- return $page !== '' && $page !== null;
- }
}
diff --git a/includes/specials/SpecialFewestrevisions.php b/includes/specials/SpecialFewestrevisions.php
index afd5ad48..65d76a65 100644
--- a/includes/specials/SpecialFewestrevisions.php
+++ b/includes/specials/SpecialFewestrevisions.php
@@ -53,15 +53,26 @@ class FewestrevisionsPage extends QueryPage {
global $wgLang, $wgContLang;
$nt = Title::makeTitleSafe( $result->namespace, $result->title );
+ if( !$nt ) {
+ return '<!-- bad title -->';
+ }
+
$text = $wgContLang->convert( $nt->getPrefixedText() );
- $plink = $skin->makeKnownLinkObj( $nt, $text );
+ $plink = $skin->linkKnown(
+ $nt,
+ $text
+ );
- $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
+ $nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape' ),
$wgLang->formatNum( $result->value ) );
- $redirect = $result->redirect ? ' - ' . wfMsg( 'isredirect' ) : '';
- $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' ) . $redirect;
-
+ $redirect = $result->redirect ? ' - ' . wfMsgHtml( 'isredirect' ) : '';
+ $nlink = $skin->linkKnown(
+ $nt,
+ $nl,
+ array(),
+ array( 'action' => 'history' )
+ ) . $redirect;
return wfSpecialList( $plink, $nlink );
}
diff --git a/includes/specials/SpecialFileDuplicateSearch.php b/includes/specials/SpecialFileDuplicateSearch.php
index 4fde0a60..0ed7020a 100644
--- a/includes/specials/SpecialFileDuplicateSearch.php
+++ b/includes/specials/SpecialFileDuplicateSearch.php
@@ -51,9 +51,12 @@ class FileDuplicateSearchPage extends QueryPage {
$nt = Title::makeTitle( NS_FILE, $result->title );
$text = $wgContLang->convert( $nt->getText() );
- $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+ $plink = $skin->link(
+ Title::newFromText( $nt->getPrefixedText() ),
+ $text
+ );
- $user = $skin->makeLinkObj( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
+ $user = $skin->link( Title::makeTitle( NS_USER, $result->img_user_text ), $result->img_user_text );
$time = $wgLang->timeanddate( $result->img_timestamp );
return "$plink . . $user . . $time";
@@ -73,7 +76,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) {
if( $title && $title->getText() != '' ) {
$dbr = wfGetDB( DB_SLAVE );
$image = $dbr->tableName( 'image' );
- $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDBKey() ) );
+ $encFilename = $dbr->addQuotes( htmlspecialchars( $title->getDBkey() ) );
$sql = "SELECT img_sha1 from $image where img_name = $encFilename";
$res = $dbr->query( $sql );
$row = $dbr->fetchRow( $res );
@@ -96,7 +99,7 @@ function wfSpecialFileDuplicateSearch( $par = null ) {
);
if( $hash != '' ) {
- $align = $wgContLang->isRtl() ? 'left' : 'right';
+ $align = $wgContLang->alignEnd();
# Show a thumbnail of the file
$img = wfFindFile( $title );
@@ -122,14 +125,14 @@ function wfSpecialFileDuplicateSearch( $par = null ) {
# Show a short summary
if( $count == 1 ) {
- $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-1">' .
- wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) .
- '</p>'
+ $wgOut->wrapWikiMsg(
+ "<p class='mw-fileduplicatesearch-result-1'>\n$1\n</p>",
+ array( 'fileduplicatesearch-result-1', $filename )
);
} elseif ( $count > 1 ) {
- $wgOut->addHTML( '<p class="mw-fileduplicatesearch-result-n">' .
- wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) .
- '</p>'
+ $wgOut->wrapWikiMsg(
+ "<p class='mw-fileduplicatesearch-result-n'>\n$1\n</p>",
+ array( 'fileduplicatesearch-result-n', $filename, $wgLang->formatNum( $count - 1 ) )
);
}
}
diff --git a/includes/specials/SpecialFilepath.php b/includes/specials/SpecialFilepath.php
index 4a724b1f..8bc1c68b 100644
--- a/includes/specials/SpecialFilepath.php
+++ b/includes/specials/SpecialFilepath.php
@@ -37,13 +37,13 @@ class FilepathForm {
}
function execute() {
- global $wgOut, $wgTitle, $wgScript;
+ global $wgOut, $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::hidden( 'title', SpecialPage::getTitleFor( 'Filepath' )->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' ) .
diff --git a/includes/specials/SpecialImport.php b/includes/specials/SpecialImport.php
index 457e03b4..6beeab7f 100644
--- a/includes/specials/SpecialImport.php
+++ b/includes/specials/SpecialImport.php
@@ -132,11 +132,11 @@ class SpecialImport extends SpecialPage {
}
private function showForm() {
- global $wgUser, $wgOut, $wgRequest, $wgTitle, $wgImportSources, $wgExportMaxLinkDepth;
+ global $wgUser, $wgOut, $wgRequest, $wgImportSources, $wgExportMaxLinkDepth;
if( !$wgUser->isAllowed( 'import' ) && !$wgUser->isAllowed( 'importupload' ) )
return $wgOut->permissionRequired( 'import' );
- $action = $wgTitle->getLocalUrl( 'action=submit' );
+ $action = $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) );
if( $wgUser->isAllowed( 'importupload' ) ) {
$wgOut->addWikiMsg( "importtext" );
@@ -273,7 +273,7 @@ class SpecialImport extends SpecialPage {
* @ingroup SpecialPage
*/
class ImportReporter {
- private $reason=false;
+ private $reason=false;
function __construct( $importer, $upload, $interwiki , $reason=false ) {
$importer->setPageOutCallback( array( $this, 'reportPage' ) );
@@ -299,7 +299,7 @@ class ImportReporter {
$contentCount = $wgContLang->formatNum( $successCount );
if( $successCount > 0 ) {
- $wgOut->addHTML( "<li>" . $skin->makeKnownLinkObj( $title ) . " " .
+ $wgOut->addHTML( "<li>" . $skin->linkKnown( $title ) . " " .
wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) .
"</li>\n"
);
@@ -309,7 +309,7 @@ class ImportReporter {
$detail = wfMsgExt( 'import-logentry-upload-detail', array( 'content', 'parsemag' ),
$contentCount );
if ( $this->reason ) {
- $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
+ $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
}
$log->addEntry( 'upload', $title, $detail );
} else {
@@ -318,7 +318,7 @@ class ImportReporter {
$detail = wfMsgExt( 'import-logentry-interwiki-detail', array( 'content', 'parsemag' ),
$contentCount, $interwiki );
if ( $this->reason ) {
- $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
+ $detail .= wfMsgForContent( 'colon-separator' ) . $this->reason;
}
$log->addEntry( 'interwiki', $title, $detail );
}
@@ -333,7 +333,8 @@ class ImportReporter {
$article->updateRevisionOn( $dbw, $nullRevision );
wfRunHooks( 'NewRevisionFromEditComplete', array($article, $nullRevision, $latest, $wgUser) );
} else {
- $wgOut->addHTML( '<li>' . wfMsgHtml( 'import-nonewrevisions' ) . '</li>' );
+ $wgOut->addHTML( "<li>" . $skin->linkKnown( $title ) . " " .
+ wfMsgHtml( 'import-nonewrevisions' ) . "</li>\n" );
}
}
diff --git a/includes/specials/SpecialIpblocklist.php b/includes/specials/SpecialIpblocklist.php
index 4ba1c811..dfdcf1a7 100644
--- a/includes/specials/SpecialIpblocklist.php
+++ b/includes/specials/SpecialIpblocklist.php
@@ -5,12 +5,13 @@
*/
/**
+ * @param $ip part of title: Special:Ipblocklist/<ip>.
* @todo document
*/
-function wfSpecialIpblocklist() {
+function wfSpecialIpblocklist( $ip = '' ) {
global $wgUser, $wgOut, $wgRequest;
-
- $ip = trim( $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ) );
+ $ip = $wgRequest->getVal( 'ip', $ip );
+ $ip = trim( $wgRequest->getVal( 'wpUnblockAddress', $ip ) );
$id = $wgRequest->getVal( 'id' );
$reason = $wgRequest->getText( 'wpUnblockReason' );
$action = $wgRequest->getText( 'action' );
@@ -94,7 +95,7 @@ class IPUnblockForm {
$titleObj = SpecialPage::getTitleFor( "Ipblocklist" );
$action = $titleObj->getLocalURL( "action=submit" );
- if ( "" != $err ) {
+ if ( $err != "" ) {
$wgOut->setSubtitle( wfMsg( "formerror" ) );
$wgOut->addWikiText( Xml::tags( 'span', array( 'class' => 'error' ), $err ) . "\n" );
}
@@ -184,8 +185,7 @@ class IPUnblockForm {
if ( !$block ) {
return array('ipb_cant_unblock', htmlspecialchars($id));
}
- if( $block->mRangeStart != $block->mRangeEnd
- && !strstr( $ip, "/" ) ) {
+ 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;
@@ -221,8 +221,7 @@ class IPUnblockForm {
function doSubmit() {
global $wgOut, $wgUser;
$retval = self::doUnblock($this->id, $this->ip, $this->reason, $range, $wgUser);
- if(!empty($retval))
- {
+ if( !empty($retval) ) {
$key = array_shift($retval);
$this->showForm(wfMsgReal($key, $retval));
return;
@@ -237,7 +236,7 @@ class IPUnblockForm {
global $wgOut, $wgUser;
$wgOut->setPagetitle( wfMsg( "ipblocklist" ) );
- if ( "" != $msg ) {
+ if ( $msg != "" ) {
$wgOut->setSubtitle( $msg );
}
@@ -264,10 +263,9 @@ class IPUnblockForm {
// Fixme -- encapsulate this sort of query-building.
$dbr = wfGetDB( DB_SLAVE );
$encIp = $dbr->addQuotes( IP::sanitizeIP($this->ip) );
- $encRange = $dbr->addQuotes( "$range%" );
$encAddr = $dbr->addQuotes( $iaddr );
$conds[] = "(ipb_address = $encIp) OR
- (ipb_range_start LIKE $encRange AND
+ (ipb_range_start" . $dbr->buildLike( $range, $dbr->anyString() ) . " AND
ipb_range_start <= $encAddr
AND ipb_range_end >= $encAddr)";
} else {
@@ -299,25 +297,48 @@ class IPUnblockForm {
$conds[] = "ipb_user != 0 OR ipb_range_end > ipb_range_start";
}
+ // Search form
+ $wgOut->addHTML( $this->searchForm() );
+
+ // Check for other blocks, i.e. global/tor blocks
+ $otherBlockLink = array();
+ wfRunHooks( 'OtherBlockLogLink', array( &$otherBlockLink, $this->ip ) );
+
+ // Show additional header for the local block only when other blocks exists.
+ // Not necessary in a standard installation without such extensions enabled
+ if( count( $otherBlockLink ) ) {
+ $wgOut->addHTML(
+ Html::rawElement( 'h2', array(), wfMsg( 'ipblocklist-localblock' ) ) . "\n"
+ );
+ }
$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->addHTML( $this->searchForm() );
$wgOut->addWikiMsg( 'ipblocklist-empty' );
}
+
+ if( count( $otherBlockLink ) ) {
+ $wgOut->addHTML(
+ Html::rawElement( 'h2', array(), wfMsgExt( 'ipblocklist-otherblocks', 'parseinline', count( $otherBlockLink ) ) ) . "\n"
+ );
+ $list = '';
+ foreach( $otherBlockLink as $link ) {
+ $list .= Html::rawElement( 'li', array(), $link ) . "\n";
+ }
+ $wgOut->addHTML( Html::rawElement( 'ul', array( 'class' => 'mw-ipblocklist-otherblocks' ), $list ) . "\n" );
+ }
+
}
function searchForm() {
- global $wgTitle, $wgScript, $wgRequest, $wgLang;
+ global $wgScript, $wgRequest, $wgLang;
$showhide = array( wfMsg( 'show' ), wfMsg( 'hide' ) );
$nondefaults = array();
@@ -345,7 +366,7 @@ class IPUnblockForm {
return
Xml::tags( 'form', array( 'action' => $wgScript ),
- Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) .
+ Xml::hidden( 'title', SpecialPage::getTitleFor( 'Ipblocklist' )->getPrefixedDbKey() ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'ipblocklist-legend' ) ) .
Xml::inputLabel( wfMsg( 'ipblocklist-username' ), 'ip', 'ip', /* size */ false, $this->ip ) .
@@ -366,7 +387,7 @@ class IPUnblockForm {
global $wgUser;
$sk = $wgUser->getSkin();
$params = $override + $options;
- $ipblocklist = SpecialPage::getTitleFor( 'IPBlockList' );
+ $ipblocklist = SpecialPage::getTitleFor( 'Ipblocklist' );
return $sk->link( $ipblocklist, htmlspecialchars( $title ),
( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) );
}
@@ -386,11 +407,10 @@ class IPUnblockForm {
if( is_null( $msg ) ) {
$msg = array();
$keys = array( 'infiniteblock', 'expiringblock', 'unblocklink', 'change-blocklink',
- 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock', 'blocklist-nousertalk' );
+ 'anononlyblock', 'createaccountblock', 'noautoblockblock', 'emailblock', 'blocklist-nousertalk', 'blocklistline' );
foreach( $keys as $key ) {
$msg[$key] = wfMsgHtml( $key );
}
- $msg['blocklistline'] = wfMsg( 'blocklistline' );
}
# Prepare links to the blocker's user and talk pages
@@ -407,7 +427,7 @@ class IPUnblockForm {
. $sk->userToolLinks( $block->mUser, $block->mAddress, false, Linker::TOOL_LINKS_NOBLOCK );
}
- $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true );
+ $formattedTime = htmlspecialchars( $wgLang->timeanddate( $block->mTimestamp, true ) );
$properties = array();
$properties[] = Block::formatExpiry( $block->mExpiry );
@@ -445,7 +465,7 @@ class IPUnblockForm {
# Create changeblocklink for all blocks with exception of autoblocks
if( !$block->mAuto ) {
- $changeblocklink = wfMsg( 'pipe-separator' ) .
+ $changeblocklink = wfMsgExt( 'pipe-separator', 'escapenoentities' ) .
$sk->link( SpecialPage::getTitleFor( 'Blockip', $block->mAddress ),
$msg['change-blocklink'],
array(), array(), 'known' );
@@ -453,7 +473,7 @@ class IPUnblockForm {
$toolLinks = "($unblocklink$changeblocklink)";
}
- $comment = $sk->commentBlock( $block->mReason );
+ $comment = $sk->commentBlock( htmlspecialchars($block->mReason) );
$s = "{$line} $comment";
if ( $block->mHideName )
diff --git a/includes/specials/SpecialLinkSearch.php b/includes/specials/SpecialLinkSearch.php
index 267ef690..5913f4b4 100644
--- a/includes/specials/SpecialLinkSearch.php
+++ b/includes/specials/SpecialLinkSearch.php
@@ -9,9 +9,7 @@
/**
* Special:LinkSearch to search the external-links table.
- * @ingroup SpecialPage
*/
-
function wfSpecialLinkSearch( $par ) {
list( $limit, $offset ) = wfCheckLimits();
@@ -48,7 +46,7 @@ function wfSpecialLinkSearch( $par ) {
$self = Title::makeTitle( NS_SPECIAL, 'Linksearch' );
- $wgOut->addWikiText( wfMsg( 'linksearch-text', '<nowiki>' . $wgLang->commaList( $wgUrlProtocols) . '</nowiki>' ) );
+ $wgOut->addWikiMsg( 'linksearch-text', '<nowiki>' . $wgLang->commaList( $wgUrlProtocols ) . '</nowiki>' );
$s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) .
Xml::hidden( 'title', $self->getPrefixedDbKey() ) .
'<fieldset>' .
@@ -96,11 +94,11 @@ class LinkSearchPage extends QueryPage {
*/
static function mungeQuery( $query , $prot ) {
$field = 'el_index';
- $rv = LinkFilter::makeLike( $query , $prot );
+ $rv = LinkFilter::makeLikeArray( $query , $prot );
if ($rv === false) {
//makeLike doesn't handle wildcard in IP, so we'll have to munge here.
if (preg_match('/^(:?[0-9]{1,3}\.)+\*\s*$|^(:?[0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]*\*\s*$/', $query)) {
- $rv = $prot . rtrim($query, " \t*") . '%';
+ $rv = array( $prot . rtrim($query, " \t*"), $dbr->anyString() );
$field = 'el_to';
}
}
@@ -125,8 +123,8 @@ class LinkSearchPage extends QueryPage {
/* strip everything past first wildcard, so that index-based-only lookup would be done */
list( $munged, $clause ) = self::mungeQuery( $this->mQuery, $this->mProt );
- $stripped = substr($munged,0,strpos($munged,'%')+1);
- $encSearch = $dbr->addQuotes( $stripped );
+ $stripped = LinkFilter::keepOneWildcard( $munged );
+ $like = $dbr->buildLike( $stripped );
$encSQL = '';
if ( isset ($this->mNs) && !$wgMiserMode )
@@ -144,14 +142,14 @@ class LinkSearchPage extends QueryPage {
$externallinks $use_index
WHERE
page_id=el_from
- AND $clause LIKE $encSearch
+ AND $clause $like
$encSQL";
}
function formatResult( $skin, $result ) {
$title = Title::makeTitle( $result->namespace, $result->title );
$url = $result->url;
- $pageLink = $skin->makeKnownLinkObj( $title );
+ $pageLink = $skin->linkKnown( $title );
$urlLink = $skin->makeExternalLink( $url, $url );
return wfMsgHtml( 'linksearch-line', $urlLink, $pageLink );
@@ -164,7 +162,7 @@ class LinkSearchPage extends QueryPage {
global $wgOut;
list( $this->mMungedQuery, $clause ) = LinkSearchPage::mungeQuery( $this->mQuery, $this->mProt );
if( $this->mMungedQuery === false ) {
- $wgOut->addWikiText( wfMsg( 'linksearch-error' ) );
+ $wgOut->addWikiMsg( 'linksearch-error' );
} else {
// For debugging
// Generates invalid xhtml with patterns that contain --
diff --git a/includes/specials/SpecialListUserRestrictions.php b/includes/specials/SpecialListUserRestrictions.php
deleted file mode 100644
index 98e7111f..00000000
--- a/includes/specials/SpecialListUserRestrictions.php
+++ /dev/null
@@ -1,162 +0,0 @@
-<?php
-
-function wfSpecialListUserRestrictions() {
- global $wgOut, $wgRequest;
-
- $wgOut->addWikiMsg( 'listuserrestrictions-intro' );
- $f = new SpecialListUserRestrictionsForm();
- $wgOut->addHTML( $f->getHTML() );
-
- if( !mt_rand( 0, 10 ) )
- UserRestriction::purgeExpired();
- $pager = new UserRestrictionsPager( $f->getConds() );
- if( $pager->getNumRows() )
- $wgOut->addHTML( $pager->getNavigationBar() .
- Xml::tags( 'ul', null, $pager->getBody() ) .
- $pager->getNavigationBar()
- );
- elseif( $f->getConds() )
- $wgOut->addWikiMsg( 'listuserrestrictions-notfound' );
- else
- $wgOut->addWikiMsg( 'listuserrestrictions-empty' );
-}
-
-class SpecialListUserRestrictionsForm {
- public function getHTML() {
- global $wgRequest, $wgScript, $wgTitle;
- $action = htmlspecialchars( $wgScript );
- $s = '';
- $s .= Xml::fieldset( wfMsg( 'listuserrestrictions-legend' ) );
- $s .= "<form action=\"{$action}\">";
- $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
- $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-type' ), 'type' ) . '&nbsp;' .
- self::typeSelector( 'type', $wgRequest->getVal( 'type' ), 'type' );
- $s .= '&nbsp;';
- $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-user' ), 'user', 'user',
- false, $wgRequest->getVal( 'user' ) );
- $s .= '<p>';
- $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-namespace' ), 'namespace' ) . '&nbsp;' .
- Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ), '', 'namespace' );
- $s .= '&nbsp;';
- $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-page' ), 'page', 'page',
- false, $wgRequest->getVal( 'page' ) );
- $s .= Xml::submitButton( wfMsg( 'listuserrestrictions-submit' ) );
- $s .= "</p></form></fieldset>";
- return $s;
- }
-
- public static function typeSelector( $name = 'type', $value = '', $id = false ) {
- $s = new XmlSelect( $name, $id, $value );
- $s->addOption( wfMsg( 'userrestrictiontype-none' ), '' );
- $s->addOption( wfMsg( 'userrestrictiontype-page' ), UserRestriction::PAGE );
- $s->addOption( wfMsg( 'userrestrictiontype-namespace' ), UserRestriction::NAMESPACE );
- return $s->getHTML();
- }
-
- public function getConds() {
- global $wgRequest;
- $conds = array();
-
- $type = $wgRequest->getVal( 'type' );
- if( in_array( $type, array( UserRestriction::PAGE, UserRestriction::NAMESPACE ) ) )
- $conds['ur_type'] = $type;
-
- $user = $wgRequest->getVal( 'user' );
- if( $user )
- $conds['ur_user_text'] = $user;
-
- $namespace = $wgRequest->getVal( 'namespace' );
- if( $namespace || $namespace === '0' )
- $conds['ur_namespace'] = $namespace;
-
- $page = $wgRequest->getVal( 'page' );
- $title = Title::newFromText( $page );
- if( $title ) {
- $conds['ur_page_namespace'] = $title->getNamespace();
- $conds['ur_page_title'] = $title->getDBKey();
- }
-
- return $conds;
- }
-}
-
-class UserRestrictionsPager extends ReverseChronologicalPager {
- public $mConds;
-
- public function __construct( $conds = array() ) {
- $this->mConds = $conds;
- parent::__construct();
- }
-
- public function getStartBody() {
- # Copied from Special:Ipblocklist
- wfProfileIn( __METHOD__ );
- # Do a link batch query
- $this->mResult->seek( 0 );
- $lb = new LinkBatch;
-
- # Faster way
- # Usernames and titles are in fact related by a simple substitution of space -> underscore
- # The last few lines of Title::secureAndSplit() tell the story.
- foreach( $this->mResult as $row ) {
- $name = str_replace( ' ', '_', $row->ur_by_text );
- $lb->add( NS_USER, $name );
- $lb->add( NS_USER_TALK, $name );
- $name = str_replace( ' ', '_', $row->ur_user_text );
- $lb->add( NS_USER, $name );
- $lb->add( NS_USER_TALK, $name );
- if( $row->ur_type == UserRestriction::PAGE )
- $lb->add( $row->ur_page_namespace, $row->ur_page_title );
- }
- $lb->execute();
- wfProfileOut( __METHOD__ );
- return '';
- }
-
- public function getQueryInfo() {
- return array(
- 'tables' => 'user_restrictions',
- 'fields' => '*',
- 'conds' => $this->mConds,
- );
- }
-
- public function formatRow( $row ) {
- return self::formatRestriction( UserRestriction::newFromRow( $row ) );
- }
-
- // Split off for use on Special:RestrictUser
- public static function formatRestriction( $r ) {
- global $wgUser, $wgLang;
- $sk = $wgUser->getSkin();
- $timestamp = $wgLang->timeanddate( $r->getTimestamp(), true );
- $blockerlink = $sk->userLink( $r->getBlockerId(), $r->getBlockerText() ) .
- $sk->userToolLinks( $r->getBlockerId(), $r->getBlockerText() );
- $subjlink = $sk->userLink( $r->getSubjectId(), $r->getSubjectText() ) .
- $sk->userToolLinks( $r->getSubjectId(), $r->getSubjectText() );
- $expiry = is_numeric( $r->getExpiry() ) ?
- wfMsg( 'listuserrestrictions-row-expiry', $wgLang->timeanddate( $r->getExpiry() ) ) :
- wfMsg( 'ipbinfinite' );
- $msg = '';
- if( $r->isNamespace() ) {
- $msg = wfMsgHtml( 'listuserrestrictions-row-ns', $subjlink,
- $wgLang->getDisplayNsText( $r->getNamespace() ), $expiry );
- }
- if( $r->isPage() ) {
- $pagelink = $sk->link( $r->getPage() );
- $msg = wfMsgHtml( 'listuserrestrictions-row-page', $subjlink,
- $pagelink, $expiry );
- }
- $reason = $sk->commentBlock( $r->getReason() );
- $removelink = '';
- if( $wgUser->isAllowed( 'restrict' ) ) {
- $removelink = '(' . $sk->link( SpecialPage::getTitleFor( 'RemoveRestrictions' ),
- wfMsgHtml( 'listuserrestrictions-remove' ), array(), array( 'id' => $r->getId() ) ) . ')';
- }
- return "<li>{$timestamp}, {$blockerlink} {$msg} {$reason} {$removelink}</li>\n";
- }
-
- public function getIndexField() {
- return 'ur_timestamp';
- }
-}
diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php
index e15b6959..b9332422 100644
--- a/includes/specials/SpecialListfiles.php
+++ b/includes/specials/SpecialListfiles.php
@@ -34,13 +34,11 @@ class ImageListPager extends TablePager {
}
$search = $wgRequest->getText( 'ilsearch' );
if ( $search != '' && !$wgMiserMode ) {
- $nt = Title::newFromUrl( $search );
+ $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}%'" );
+ $this->mQueryConds = array( 'LOWER(img_name)' . $dbr->buildLike( $dbr->anyString(),
+ strtolower( $nt->getDBkey() ), $dbr->anyString() ) );
}
}
@@ -127,21 +125,23 @@ class ImageListPager extends TablePager {
global $wgLang;
switch ( $field ) {
case 'img_timestamp':
- return $wgLang->timeanddate( $value, true );
+ return htmlspecialchars( $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_FILE, $name ), $value );
+ $link = $this->getSkin()->linkKnown( Title::makeTitle( NS_FILE, $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 ) );
+ $link = $this->getSkin()->link(
+ Title::makeTitle( NS_USER, $value ),
+ htmlspecialchars( $value )
+ );
} else {
$link = htmlspecialchars( $value );
}
@@ -156,10 +156,10 @@ class ImageListPager extends TablePager {
}
function getForm() {
- global $wgRequest, $wgMiserMode;
+ global $wgRequest, $wgScript, $wgMiserMode;
$search = $wgRequest->getText( 'ilsearch' );
- $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $this->getTitle()->getLocalURL(), 'id' => 'mw-listfiles-form' ) ) .
+ $s = Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', null, wfMsg( 'listfiles' ) ) .
Xml::tags( 'label', null, wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) );
diff --git a/includes/specials/SpecialListgrouprights.php b/includes/specials/SpecialListgrouprights.php
index d1fc0818..83724a4f 100644
--- a/includes/specials/SpecialListgrouprights.php
+++ b/includes/specials/SpecialListgrouprights.php
@@ -25,14 +25,15 @@ class SpecialListGroupRights extends SpecialPage {
*/
public function execute( $par ) {
global $wgOut, $wgImplicitGroups, $wgMessageCache;
- global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups;
+ global $wgGroupPermissions, $wgRevokePermissions, $wgAddGroups, $wgRemoveGroups;
+ global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
$wgMessageCache->loadAllMessages();
$this->setHeaders();
$this->outputHeader();
$wgOut->addHTML(
- Xml::openElement( 'table', array( 'class' => 'mw-listgrouprights-table' ) ) .
+ Xml::openElement( 'table', array( 'class' => 'wikitable mw-listgrouprights-table' ) ) .
'<tr>' .
Xml::element( 'th', null, wfMsg( 'listgrouprights-group' ) ) .
Xml::element( 'th', null, wfMsg( 'listgrouprights-rights' ) ) .
@@ -40,7 +41,7 @@ class SpecialListGroupRights extends SpecialPage {
);
foreach( $wgGroupPermissions as $group => $permissions ) {
- $groupname = ( $group == '*' ) ? 'all' : htmlspecialchars( $group ); // Replace * with a more descriptive groupname
+ $groupname = ( $group == '*' ) ? 'all' : $group; // Replace * with a more descriptive groupname
$msg = wfMsg( 'group-' . $groupname );
if ( wfEmptyMsg( 'group-' . $groupname, $msg ) || $msg == '' ) {
@@ -58,23 +59,41 @@ class SpecialListGroupRights extends SpecialPage {
if( $group == '*' ) {
// Do not make a link for the generic * group
- $grouppage = $groupnameLocalized;
+ $grouppage = htmlspecialchars($groupnameLocalized);
} else {
- $grouppage = $this->skin->makeLink( $grouppageLocalized, $groupnameLocalized );
+ $grouppage = $this->skin->link(
+ Title::newFromText( $grouppageLocalized ),
+ htmlspecialchars($groupnameLocalized)
+ );
}
if ( $group === 'user' ) {
// Link to Special:listusers for implicit group 'user'
- $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), '' );
+ $grouplink = '<br />' . $this->skin->link(
+ SpecialPage::getTitleFor( 'Listusers' ),
+ wfMsgHtml( 'listgrouprights-members' ),
+ array(),
+ array(),
+ array( 'known', 'noclasses' )
+ );
} elseif ( !in_array( $group, $wgImplicitGroups ) ) {
- $grouplink = '<br />' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group );
+ $grouplink = '<br />' . $this->skin->link(
+ SpecialPage::getTitleFor( 'Listusers' ),
+ wfMsgHtml( 'listgrouprights-members' ),
+ array(),
+ array( 'group' => $group ),
+ array( 'known', 'noclasses' )
+ );
} else {
// No link to Special:listusers for other implicit groups as they are unlistable
$grouplink = '';
}
+ $revoke = isset( $wgRevokePermissions[$group] ) ? $wgRevokePermissions[$group] : array();
$addgroups = isset( $wgAddGroups[$group] ) ? $wgAddGroups[$group] : array();
$removegroups = isset( $wgRemoveGroups[$group] ) ? $wgRemoveGroups[$group] : array();
+ $addgroupsSelf = isset( $wgGroupsAddToSelf[$group] ) ? $wgGroupsAddToSelf[$group] : array();
+ $removegroupsSelf = isset( $wgGroupsRemoveFromSelf[$group] ) ? $wgGroupsRemoveFromSelf[$group] : array();
$wgOut->addHTML(
'<tr>
@@ -82,30 +101,47 @@ class SpecialListGroupRights extends SpecialPage {
$grouppage . $grouplink .
'</td>
<td>' .
- self::formatPermissions( $permissions, $addgroups, $removegroups ) .
+ self::formatPermissions( $permissions, $revoke, $addgroups, $removegroups, $addgroupsSelf, $removegroupsSelf ) .
'</td>
</tr>'
);
}
$wgOut->addHTML(
- Xml::closeElement( 'table' ) . "\n"
+ Xml::closeElement( 'table' ) . "\n<br /><hr />\n"
);
+ $wgOut->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
}
/**
* Create a user-readable list of permissions from the given array.
*
* @param $permissions Array of permission => bool (from $wgGroupPermissions items)
+ * @param $revoke Array of permission => bool (from $wgRevokePermissions items)
+ * @param $add Array of groups this group is allowed to add or true
+ * @param $remove Array of groups this group is allowed to remove or true
+ * @param $addSelf Array of groups this group is allowed to add to self or true
+ * @param $removeSelf Array of group this group is allowed to remove from self or true
* @return string List of all granted permissions, separated by comma separator
*/
- private static function formatPermissions( $permissions, $add, $remove ) {
+ private static function formatPermissions( $permissions, $revoke, $add, $remove, $addSelf, $removeSelf ) {
global $wgLang;
+
$r = array();
foreach( $permissions as $permission => $granted ) {
- if ( $granted ) {
+ //show as granted only if it isn't revoked to prevent duplicate display of permissions
+ if( $granted && ( !isset( $revoke[$permission] ) || !$revoke[$permission] ) ) {
$description = wfMsgExt( 'listgrouprights-right-display', array( 'parseinline' ),
User::getRightDescription( $permission ),
- $permission
+ '<span class="mw-listgrouprights-right-name">' . $permission . '</span>'
+ );
+ $r[] = $description;
+ }
+ }
+ foreach( $revoke as $permission => $revoked ) {
+ if( $revoked ) {
+ $description = wfMsgExt( 'listgrouprights-right-revoked', array( 'parseinline' ),
+ User::getRightDescription( $permission ),
+ '<span class="mw-listgrouprights-right-name">' . $permission . '</span>'
);
$r[] = $description;
}
@@ -114,13 +150,27 @@ class SpecialListGroupRights extends SpecialPage {
if( $add === true ){
$r[] = wfMsgExt( 'listgrouprights-addgroup-all', array( 'escape' ) );
} else if( is_array( $add ) && count( $add ) ) {
+ $add = array_values( array_unique( $add ) );
$r[] = wfMsgExt( 'listgrouprights-addgroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $add ) ), count( $add ) );
}
if( $remove === true ){
$r[] = wfMsgExt( 'listgrouprights-removegroup-all', array( 'escape' ) );
} else if( is_array( $remove ) && count( $remove ) ) {
+ $remove = array_values( array_unique( $remove ) );
$r[] = wfMsgExt( 'listgrouprights-removegroup', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $remove ) ), count( $remove ) );
}
+ if( $addSelf === true ){
+ $r[] = wfMsgExt( 'listgrouprights-addgroup-self-all', array( 'escape' ) );
+ } else if( is_array( $addSelf ) && count( $addSelf ) ) {
+ $addSelf = array_values( array_unique( $addSelf ) );
+ $r[] = wfMsgExt( 'listgrouprights-addgroup-self', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $addSelf ) ), count( $addSelf ) );
+ }
+ if( $removeSelf === true ){
+ $r[] = wfMsgExt( 'listgrouprights-removegroup-self-all', array( 'escape' ) );
+ } else if( is_array( $removeSelf ) && count( $removeSelf ) ) {
+ $removeSelf = array_values( array_unique( $removeSelf ) );
+ $r[] = wfMsgExt( 'listgrouprights-removegroup-self', array( 'parseinline' ), $wgLang->listToText( array_map( array( 'User', 'makeGroupLinkWiki' ), $removeSelf ) ), count( $removeSelf ) );
+ }
if( empty( $r ) ) {
return '';
} else {
diff --git a/includes/specials/SpecialListredirects.php b/includes/specials/SpecialListredirects.php
index 9555bd16..bf594070 100644
--- a/includes/specials/SpecialListredirects.php
+++ b/includes/specials/SpecialListredirects.php
@@ -32,7 +32,12 @@ class ListredirectsPage extends QueryPage {
# Make a link to the redirect itself
$rd_title = Title::makeTitle( $result->namespace, $result->title );
- $rd_link = $skin->makeLinkObj( $rd_title, '', 'redirect=no' );
+ $rd_link = $skin->link(
+ $rd_title,
+ null,
+ array(),
+ array( 'redirect' => 'no' )
+ );
# Find out where the redirect leads
$revision = Revision::newFromTitle( $rd_title );
@@ -41,7 +46,7 @@ class ListredirectsPage extends QueryPage {
$target = Title::newFromRedirect( $revision->getText() );
if( $target ) {
$arr = $wgContLang->getArrow() . $wgContLang->getDirMark();
- $targetLink = $skin->makeLinkObj( $target );
+ $targetLink = $skin->link( $target );
return "$rd_link $arr $targetLink";
} else {
return "<s>$rd_link</s>";
diff --git a/includes/specials/SpecialListusers.php b/includes/specials/SpecialListusers.php
index aa057801..bdb59980 100644
--- a/includes/specials/SpecialListusers.php
+++ b/includes/specials/SpecialListusers.php
@@ -71,10 +71,12 @@ class UsersPager extends AlphabeticPager {
}
function getQueryInfo() {
+ global $wgUser;
$dbr = wfGetDB( DB_SLAVE );
$conds = array();
// Don't show hidden names
- $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0';
+ if( !$wgUser->isAllowed('hideuser') )
+ $conds[] = 'ipb_deleted IS NULL';
if( $this->requestedGroup != '' ) {
$conds['ug_group'] = $this->requestedGroup;
$useIndex = '';
@@ -84,7 +86,7 @@ class UsersPager extends AlphabeticPager {
if( $this->requestedUser != '' ) {
# Sorted either by account creation or name
if( $this->creationSort ) {
- $conds[] = 'user_id >= ' . User::idFromName( $this->requestedUser );
+ $conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) );
} else {
$conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
}
@@ -97,14 +99,16 @@ class UsersPager extends AlphabeticPager {
$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 ",
+ LEFT JOIN $ipblocks ON user_id=ipb_user AND ipb_deleted=1 AND ipb_auto=0 ",
'fields' => array(
$this->creationSort ? 'MAX(user_name) AS user_name' : 'user_name',
$this->creationSort ? 'user_id' : 'MAX(user_id) AS user_id',
'MAX(user_editcount) AS edits',
'COUNT(ug_group) AS numgroups',
- 'MAX(ug_group) AS singlegroup',
- 'MIN(user_registration) AS creation'),
+ 'MAX(ug_group) AS singlegroup', // the usergroup if there is only one
+ 'MIN(user_registration) AS creation',
+ 'MAX(ipb_deleted) AS ipb_deleted' // block/hide status
+ ),
'options' => array('GROUP BY' => $this->creationSort ? 'user_id' : 'user_name'),
'conds' => $conds
);
@@ -117,7 +121,7 @@ class UsersPager extends AlphabeticPager {
global $wgLang;
$userPage = Title::makeTitle( NS_USER, $row->user_name );
- $name = $this->getSkin()->makeLinkObj( $userPage, htmlspecialchars( $userPage->getText() ) );
+ $name = $this->getSkin()->link( $userPage, htmlspecialchars( $userPage->getText() ) );
if( $row->numgroups > 1 || ( $this->requestedGroup && $row->numgroups == 1 ) ) {
$list = array();
@@ -131,11 +135,14 @@ class UsersPager extends AlphabeticPager {
}
$item = wfSpecialList( $name, $groups );
+ if( $row->ipb_deleted ) {
+ $item = "<span class=\"deleted\">$item</span>";
+ }
global $wgEdititis;
if ( $wgEdititis ) {
$editCount = $wgLang->formatNum( $row->edits );
- $edits = ' [' . wfMsgExt( 'usereditcount', 'parsemag', $editCount ) . ']';
+ $edits = ' [' . wfMsgExt( 'usereditcount', array( 'parsemag', 'escape' ), $editCount ) . ']';
} else {
$edits = '';
}
@@ -145,7 +152,8 @@ class UsersPager extends AlphabeticPager {
if( $row->creation ) {
$d = $wgLang->date( wfTimestamp( TS_MW, $row->creation ), true );
$t = $wgLang->time( wfTimestamp( TS_MW, $row->creation ), true );
- $created = ' (' . wfMsgHtml( 'usercreated', $d, $t ) . ')';
+ $created = ' (' . wfMsg( 'usercreated', $d, $t ) . ')';
+ $created = htmlspecialchars( $created );
}
wfRunHooks( 'SpecialListusersFormatRow', array( &$item, $row ) );
@@ -185,11 +193,11 @@ class UsersPager extends AlphabeticPager {
Xml::option( wfMsg( 'group-all' ), '' );
foreach( $this->getAllGroups() as $group => $groupText )
$out .= Xml::option( $groupText, $group, $group == $this->requestedGroup );
- $out .= Xml::closeElement( 'select' ) . '<br/>';
+ $out .= Xml::closeElement( 'select' ) . '<br />';
$out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly );
$out .= '&nbsp;';
$out .= Xml::checkLabel( wfMsg('listusers-creationsort'), 'creationSort', 'creationSort', $this->creationSort );
- $out .= '<br/>';
+ $out .= '<br />';
wfRunHooks( 'SpecialListusersHeaderForm', array( $this, &$out ) );
@@ -233,7 +241,7 @@ class UsersPager extends AlphabeticPager {
/**
* Get a list of groups the specified user belongs to
*
- * @param int $uid
+ * @param $uid Integer: user id
* @return array
*/
protected static function getGroups( $uid ) {
@@ -245,13 +253,13 @@ class UsersPager extends AlphabeticPager {
/**
* Format a link to a group description page
*
- * @param string $group
+ * @param $group String: group name
* @return string
*/
protected static function buildGroupLink( $group ) {
static $cache = array();
if( !isset( $cache[$group] ) )
- $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupMember( $group ) );
+ $cache[$group] = User::makeGroupLinkHtml( $group, htmlspecialchars( User::getGroupMember( $group ) ) );
return $cache[$group];
}
}
diff --git a/includes/specials/SpecialLockdb.php b/includes/specials/SpecialLockdb.php
index 5859d5b2..8c701dd6 100644
--- a/includes/specials/SpecialLockdb.php
+++ b/includes/specials/SpecialLockdb.php
@@ -53,7 +53,7 @@ class DBLockForm {
$wgOut->setPagetitle( wfMsg( 'lockdb' ) );
$wgOut->addWikiMsg( 'lockdbtext' );
- if ( "" != $err ) {
+ if ( $err != "" ) {
$wgOut->setSubtitle( wfMsg( 'formerror' ) );
$wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
}
@@ -65,7 +65,7 @@ class DBLockForm {
$reason = htmlspecialchars( $this->reason );
$token = htmlspecialchars( $wgUser->editToken() );
- $wgOut->addHTML( <<<END
+ $wgOut->addHTML( <<<HTML
<form id="lockdb" method="post" action="{$action}">
{$elr}:
<textarea name="wpLockReason" rows="10" cols="60" wrap="virtual">{$reason}</textarea>
@@ -85,7 +85,7 @@ class DBLockForm {
</table>
<input type="hidden" name="wpEditToken" value="{$token}" />
</form>
-END
+HTML
);
}
diff --git a/includes/specials/SpecialLog.php b/includes/specials/SpecialLog.php
index 2382344b..d1ccc8c4 100644
--- a/includes/specials/SpecialLog.php
+++ b/includes/specials/SpecialLog.php
@@ -52,9 +52,19 @@ function wfSpecialLog( $par = '' ) {
$y = '';
$m = '';
}
+ # Handle type-specific inputs
+ $qc = array();
+ if( $type == 'suppress' ) {
+ $offender = User::newFromName( $wgRequest->getVal('offender'), false );
+ if( $offender && $offender->getId() > 0 ) {
+ $qc = array( 'ls_field' => 'target_author_id', 'ls_value' => $offender->getId() );
+ } else if( $offender && IP::isIPAddress( $offender->getName() ) ) {
+ $qc = array( 'ls_field' => 'target_author_ip', 'ls_value' => $offender->getName() );
+ }
+ }
# 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, $tagFilter );
+ $pager = new LogPager( $loglist, $type, $user, $title, $pattern, $qc, $y, $m, $tagFilter );
# Set title and add header
$loglist->showHeader( $pager->getType() );
# Show form options
diff --git a/includes/specials/SpecialMIMEsearch.php b/includes/specials/SpecialMIMEsearch.php
index cdfde24e..dafe003e 100644
--- a/includes/specials/SpecialMIMEsearch.php
+++ b/includes/specials/SpecialMIMEsearch.php
@@ -65,15 +65,20 @@ class MIMEsearchPage extends QueryPage {
$nt = Title::makeTitle( $result->namespace, $result->title );
$text = $wgContLang->convert( $nt->getText() );
- $plink = $skin->makeLink( $nt->getPrefixedText(), $text );
+ $plink = $skin->link(
+ Title::newFromText( $nt->getPrefixedText() ),
+ htmlspecialchars( $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 );
+ $dimensions = htmlspecialchars( wfMsg( 'widthheight',
+ $wgLang->formatNum( $result->img_width ),
+ $wgLang->formatNum( $result->img_height )
+ ) );
+ $user = $skin->link( Title::makeTitle( NS_USER, $result->img_user_text ), htmlspecialchars( $result->img_user_text ) );
+ $time = htmlspecialchars( $wgLang->timeanddate( $result->img_timestamp ) );
return "($download) $plink . . $dimensions . . $bytes . . $user . . $time";
}
@@ -83,13 +88,14 @@ class MIMEsearchPage extends QueryPage {
* Output the HTML search form, and constructs the MIMEsearchPage object.
*/
function wfSpecialMIMEsearch( $par = null ) {
- global $wgRequest, $wgTitle, $wgOut;
+ global $wgRequest, $wgOut;
$mime = isset( $par ) ? $par : $wgRequest->getText( 'mime' );
$wgOut->addHTML(
- Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => $wgTitle->getLocalUrl() ) ) .
+ Xml::openElement( 'form', array( 'id' => 'specialmimesearch', 'method' => 'get', 'action' => SpecialPage::getTitleFor( 'MIMEsearch' )->getLocalUrl() ) ) .
Xml::openElement( 'fieldset' ) .
+ Xml::hidden( 'title', SpecialPage::getTitleFor( 'MIMEsearch' )->getPrefixedText() ) .
Xml::element( 'legend', null, wfMsg( 'mimesearch' ) ) .
Xml::inputLabel( wfMsg( 'mimetype' ), 'mime', 'mime', 20, $mime ) . ' ' .
Xml::submitButton( wfMsg( 'ilsubmit' ) ) .
diff --git a/includes/specials/SpecialMergeHistory.php b/includes/specials/SpecialMergeHistory.php
index c51ce7c3..1b4ef30c 100644
--- a/includes/specials/SpecialMergeHistory.php
+++ b/includes/specials/SpecialMergeHistory.php
@@ -67,7 +67,7 @@ class MergehistoryForm {
}
function execute() {
- global $wgOut, $wgUser;
+ global $wgOut;
$wgOut->setPagetitle( wfMsgHtml( "mergehistory" ) );
@@ -155,7 +155,7 @@ class MergehistoryForm {
$haveRevisions = $revisions && $revisions->getNumRows() > 0;
$titleObj = SpecialPage::getTitleFor( "Mergehistory" );
- $action = $titleObj->getLocalURL( "action=submit" );
+ $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) );
# Start the form here
$top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'merge' ) );
$wgOut->addHTML( $top );
@@ -218,7 +218,7 @@ class MergehistoryForm {
}
function formatRevisionRow( $row ) {
- global $wgUser, $wgLang;
+ global $wgLang;
$rev = new Revision( $row );
@@ -228,8 +228,12 @@ class MergehistoryForm {
$ts = wfTimestamp( TS_MW, $row->rev_timestamp );
$checkBox = Xml::radio( "mergepoint", $ts, false );
- $pageLink = $this->sk->makeKnownLinkObj( $rev->getTitle(),
- htmlspecialchars( $wgLang->timeanddate( $ts ) ), 'oldid=' . $rev->getId() );
+ $pageLink = $this->sk->linkKnown(
+ $rev->getTitle(),
+ htmlspecialchars( $wgLang->timeanddate( $ts ) ),
+ array(),
+ array( 'oldid' => $rev->getId() )
+ );
if( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
$pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
}
@@ -238,8 +242,15 @@ class MergehistoryForm {
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] );
+ $last = $this->sk->linkKnown(
+ $rev->getTitle(),
+ $this->message['last'],
+ array(),
+ array(
+ 'diff' => $row->rev_id,
+ 'oldid' => $this->prevId[$row->rev_id]
+ )
+ );
$userLink = $this->sk->revUserTools( $rev );
@@ -261,8 +272,15 @@ class MergehistoryForm {
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" );
+ $link = $this->sk->linkKnown(
+ $titleObj,
+ $wgLang->timeanddate( $ts, true ),
+ array(),
+ array(
+ 'target' => $target,
+ 'timestamp' => $ts
+ )
+ );
if( $this->isDeleted($row, Revision::DELETED_TEXT) )
$link = '<span class="history-deleted">' . $link . '</span>';
return $link;
@@ -270,7 +288,7 @@ class MergehistoryForm {
}
function merge() {
- global $wgOut, $wgUser;
+ global $wgOut;
# 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...
diff --git a/includes/specials/SpecialMostlinked.php b/includes/specials/SpecialMostlinked.php
index 078489bd..f112ae17 100644
--- a/includes/specials/SpecialMostlinked.php
+++ b/includes/specials/SpecialMostlinked.php
@@ -22,22 +22,35 @@ class MostlinkedPage extends QueryPage {
function isExpensive() { return true; }
function isSyndicated() { return false; }
- /**
- * Note: Getting page_namespace only works if $this->isCached() is false
- */
function getSQL() {
+ global $wgMiserMode;
+
$dbr = wfGetDB( DB_SLAVE );
+
+ # In miser mode, reduce the query cost by adding a threshold for large wikis
+ if ( $wgMiserMode ) {
+ $numPages = SiteStats::pages();
+ if ( $numPages > 10000 ) {
+ $cutoff = 100;
+ } elseif ( $numPages > 100 ) {
+ $cutoff = intval( sqrt( $numPages ) );
+ } else {
+ $cutoff = 1;
+ }
+ } else {
+ $cutoff = 1;
+ }
+
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
+ COUNT(*) AS value
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";
+ GROUP BY pl_namespace, pl_title
+ HAVING COUNT(*) > $cutoff";
}
/**
@@ -57,12 +70,13 @@ class MostlinkedPage extends QueryPage {
* Make a link to "what links here" for the specified title
*
* @param $title Title being queried
+ * @param $caption String: text to display on the link
* @param $skin Skin to use
- * @return string
+ * @return String
*/
function makeWlhLink( &$title, $caption, &$skin ) {
$wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedDBkey() );
- return $skin->makeKnownLinkObj( $wlh, $caption );
+ return $skin->linkKnown( $wlh, $caption );
}
/**
@@ -75,7 +89,10 @@ class MostlinkedPage extends QueryPage {
function formatResult( $skin, $result ) {
global $wgLang;
$title = Title::makeTitleSafe( $result->namespace, $result->title );
- $link = $skin->makeLinkObj( $title );
+ if ( !$title ) {
+ return '<!-- ' . htmlspecialchars( "Invalid title: [[$title]]" ) . ' -->';
+ }
+ $link = $skin->link( $title );
$wlh = $this->makeWlhLink( $title,
wfMsgExt( 'nlinks', array( 'parsemag', 'escape'),
$wgLang->formatNum( $result->value ) ), $skin );
diff --git a/includes/specials/SpecialMostlinkedcategories.php b/includes/specials/SpecialMostlinkedcategories.php
index ab250675..20a35c97 100644
--- a/includes/specials/SpecialMostlinkedcategories.php
+++ b/includes/specials/SpecialMostlinkedcategories.php
@@ -58,7 +58,7 @@ class MostlinkedCategoriesPage extends QueryPage {
$nt = Title::makeTitle( $result->namespace, $result->title );
$text = $wgContLang->convert( $nt->getText() );
- $plink = $skin->makeLinkObj( $nt, htmlspecialchars( $text ) );
+ $plink = $skin->link( $nt, htmlspecialchars( $text ) );
$nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
$wgLang->formatNum( $result->value ) );
diff --git a/includes/specials/SpecialMostlinkedtemplates.php b/includes/specials/SpecialMostlinkedtemplates.php
index 2d398a38..71a6b539 100644
--- a/includes/specials/SpecialMostlinkedtemplates.php
+++ b/includes/specials/SpecialMostlinkedtemplates.php
@@ -16,7 +16,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Name of the report
*
- * @return string
+ * @return String
*/
public function getName() {
return 'Mostlinkedtemplates';
@@ -25,7 +25,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Is this report expensive, i.e should it be cached?
*
- * @return bool
+ * @return Boolean
*/
public function isExpensive() {
return true;
@@ -34,7 +34,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Is there a feed available?
*
- * @return bool
+ * @return Boolean
*/
public function isSyndicated() {
return false;
@@ -43,7 +43,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Sort the results in descending order?
*
- * @return bool
+ * @return Boolean
*/
public function sortDescending() {
return true;
@@ -52,7 +52,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Generate SQL for the report
*
- * @return string
+ * @return String
*/
public function getSql() {
$dbr = wfGetDB( DB_SLAVE );
@@ -70,8 +70,8 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Pre-cache page existence to speed up link generation
*
- * @param Database $dbr Database connection
- * @param int $res Result pointer
+ * @param $db Database connection
+ * @param $res ResultWrapper
*/
public function preprocessResults( $db, $res ) {
$batch = new LinkBatch();
@@ -86,16 +86,15 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Format a result row
*
- * @param Skin $skin Skin to use for UI elements
- * @param object $result Result row
- * @return string
+ * @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 );
- $skin->link( $title );
return wfSpecialList(
- $skin->makeLinkObj( $title ),
+ $skin->link( $title ),
$this->makeWlhLink( $title, $skin, $result )
);
}
@@ -103,10 +102,10 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* 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
+ * @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;
@@ -120,7 +119,7 @@ class SpecialMostlinkedtemplates extends QueryPage {
/**
* Execution function
*
- * @param mixed $par Parameters passed to the page
+ * @param $par Mixed: parameters passed to the page
*/
function wfSpecialMostlinkedtemplates( $par = false ) {
list( $limit, $offset ) = wfCheckLimits();
diff --git a/includes/specials/SpecialMostrevisions.php b/includes/specials/SpecialMostrevisions.php
index f5a0f8c0..414e8d97 100644
--- a/includes/specials/SpecialMostrevisions.php
+++ b/includes/specials/SpecialMostrevisions.php
@@ -42,11 +42,16 @@ class MostrevisionsPage extends QueryPage {
$nt = Title::makeTitle( $result->namespace, $result->title );
$text = $wgContLang->convert( $nt->getPrefixedText() );
- $plink = $skin->makeKnownLinkObj( $nt, $text );
+ $plink = $skin->linkKnown( $nt, $text );
$nl = wfMsgExt( 'nrevisions', array( 'parsemag', 'escape'),
$wgLang->formatNum( $result->value ) );
- $nlink = $skin->makeKnownLinkObj( $nt, $nl, 'action=history' );
+ $nlink = $skin->linkKnown(
+ $nt,
+ $nl,
+ array(),
+ array( 'action' => 'history' )
+ );
return wfSpecialList($plink, $nlink);
}
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
index 8fcf33a9..02197b19 100644
--- a/includes/specials/SpecialMovepage.php
+++ b/includes/specials/SpecialMovepage.php
@@ -17,7 +17,9 @@ function wfSpecialMovepage( $par = null ) {
}
$target = isset( $par ) ? $par : $wgRequest->getVal( 'target' );
- $oldTitleText = $wgRequest->getText( 'wpOldTitle', $target );
+
+ // Yes, the use of getVal() and getText() is wanted, see bug 20365
+ $oldTitleText = $wgRequest->getVal( 'wpOldTitle', $target );
$newTitleText = $wgRequest->getText( 'wpNewTitle' );
$oldTitle = Title::newFromText( $oldTitleText );
@@ -56,12 +58,12 @@ function wfSpecialMovepage( $par = null ) {
class MovePageForm {
var $oldTitle, $newTitle; # Objects
var $reason; # Text input
- var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect; # Checks
+ var $moveTalk, $deleteAndMove, $moveSubpages, $fixRedirects, $leaveRedirect, $moveOverShared; # Checks
private $watch = false;
function __construct( $oldTitle, $newTitle ) {
- global $wgRequest;
+ global $wgRequest, $wgUser;
$target = isset($par) ? $par : $wgRequest->getVal( 'target' );
$this->oldTitle = $oldTitle;
$this->newTitle = $newTitle;
@@ -77,7 +79,8 @@ class MovePageForm {
}
$this->moveSubpages = $wgRequest->getBool( 'wpMovesubpages', false );
$this->deleteAndMove = $wgRequest->getBool( 'wpDeleteAndMove' ) && $wgRequest->getBool( 'wpConfirm' );
- $this->watch = $wgRequest->getCheck( 'wpWatch' );
+ $this->moveOverShared = $wgRequest->getBool( 'wpMoveOverSharedFile', false );
+ $this->watch = $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn();
}
/**
@@ -87,11 +90,11 @@ class MovePageForm {
* OutputPage::wrapWikiMsg().
*/
function showForm( $err ) {
- global $wgOut, $wgUser, $wgFixDoubleRedirects;
+ global $wgOut, $wgUser, $wgContLang, $wgFixDoubleRedirects;
$skin = $wgUser->getSkin();
- $oldTitleLink = $skin->makeLinkObj( $this->oldTitle );
+ $oldTitleLink = $skin->link( $this->oldTitle );
$wgOut->setPagetitle( wfMsg( 'move-page', $this->oldTitle->getPrefixedText() ) );
$wgOut->setSubtitle( wfMsg( 'move-page-backlink', $oldTitleLink ) );
@@ -128,12 +131,21 @@ class MovePageForm {
</tr>";
$err = '';
} else {
+ if ($this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) {
+ $wgOut->wrapWikiMsg( "<div class=\"error mw-moveuserpage-warning\">\n$1\n</div>", 'moveuserpage-warning' );
+ }
$wgOut->addWikiMsg( 'movepagetext' );
$movepagebtn = wfMsg( 'movepagebtn' );
$submitVar = 'wpMove';
$confirm = false;
}
+ if ( !empty($err) && $err[0] == 'file-exists-sharedrepo' && $wgUser->isAllowed( 'reupload-shared' ) ) {
+ $wgOut->addWikiMsg( 'move-over-sharedrepo', $newTitle->getPrefixedText() );
+ $submitVar = 'wpMoveOverSharedFile';
+ $err = '';
+ }
+
$oldTalk = $this->oldTitle->getTalkPage();
$considerTalk = ( !$this->oldTitle->isTalkPage() && $oldTalk->exists() );
@@ -166,6 +178,22 @@ class MovePageForm {
}
}
+ if ( $this->oldTitle->isProtected( 'move' ) ) {
+ # Is the title semi-protected?
+ if ( $this->oldTitle->isSemiProtected( 'move' ) ) {
+ $noticeMsg = 'semiprotectedpagemovewarning';
+ $classes[] = 'mw-textarea-sprotected';
+ } else {
+ # Then it must be protected based on static groups (regular)
+ $noticeMsg = 'protectedpagemovewarning';
+ $classes[] = 'mw-textarea-protected';
+ }
+ $wgOut->addHTML( "<div class='mw-warning-with-logexcerpt'>\n" );
+ $wgOut->addWikiMsg( $noticeMsg );
+ LogEventsList::showLogExtract( $wgOut, 'protect', $this->oldTitle->getPrefixedText(), '', array( 'lim' => 1 ) );
+ $wgOut->addHTML( "</div>\n" );
+ }
+
$wgOut->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), 'id' => 'movepage' ) ) .
Xml::openElement( 'fieldset' ) .
@@ -184,7 +212,7 @@ class MovePageForm {
Xml::label( wfMsg( 'newtitle' ), 'wpNewTitle' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'wpNewTitle', 40, $newTitle->getPrefixedText(), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
+ Xml::input( 'wpNewTitle', 40, $wgContLang->recodeForEdit( $newTitle->getPrefixedText() ), array( 'type' => 'text', 'id' => 'wpNewTitle' ) ) .
Xml::hidden( 'wpOldTitle', $this->oldTitle->getPrefixedText() ) .
"</td>
</tr>
@@ -193,7 +221,8 @@ class MovePageForm {
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 ) ) .
+ Html::element( 'textarea', array( 'name' => 'wpReason', 'id' => 'wpReason', 'cols' => 60, 'rows' => 2,
+ 'maxlength' => 200 ), $this->reason ) .
"</td>
</tr>"
);
@@ -242,34 +271,43 @@ class MovePageForm {
<tr>
<td></td>
<td class=\"mw-input\">" .
- Xml::checkLabel( wfMsgExt(
+ Xml::check(
+ '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),
+ array( 'id' => 'wpMovesubpages' )
+ ) . '&nbsp;' .
+ Xml::tags( 'label', array( 'for' => 'wpMovesubpages' ),
+ wfMsgExt(
( $this->oldTitle->hasSubpages()
? 'move-subpages'
: 'move-talk-subpages' ),
- array( 'parsemag' ),
+ array( 'parseinline' ),
$wgLang->formatNum( $wgMaximumMovedPages ),
# $2 to allow use of PLURAL in message.
$wgMaximumMovedPages
- ),
- '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( "
+ $watchChecked = $wgUser->isLoggedIn() && ($this->watch || $wgUser->getBoolOption( 'watchmoves' )
+ || $this->oldTitle->userIsWatching());
+ # Don't allow watching if user is not logged in
+ if( $wgUser->isLoggedIn() ) {
+ $wgOut->addHTML( "
<tr>
<td></td>
<td class='mw-input'>" .
Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) .
"</td>
- </tr>
+ </tr>");
+ }
+
+ $wgOut->addHTML( "
{$confirm}
<tr>
<td>&nbsp;</td>
@@ -329,12 +367,24 @@ class MovePageForm {
return;
}
+ # Show a warning if the target file exists on a shared repo
+ if ( $nt->getNamespace() == NS_FILE
+ && !( $this->moveOverShared && $wgUser->isAllowed( 'reupload-shared' ) )
+ && !RepoGroup::singleton()->getLocalRepo()->findFile( $nt )
+ && wfFindFile( $nt ) )
+ {
+ $this->showForm( array('file-exists-sharedrepo') );
+ return;
+
+ }
+
if ( $wgUser->isAllowed( 'suppressredirect' ) ) {
$createRedirect = $this->leaveRedirect;
} else {
$createRedirect = true;
}
+ # Do the actual move.
$error = $ot->moveTo( $nt, true, $this->reason, $createRedirect );
if ( $error !== true ) {
# FIXME: show all the errors in a list, not just the first one
@@ -393,7 +443,7 @@ class MovePageForm {
)
) ) {
$conds = array(
- 'page_title LIKE '.$dbr->addQuotes( $dbr->escapeLike( $ot->getDBkey() ) . '/%' )
+ 'page_title' . $dbr->buildLike( $ot->getDBkey() . '/', $dbr->anyString() )
.' OR page_title = ' . $dbr->addQuotes( $ot->getDBkey() )
);
$conds['page_namespace'] = array();
@@ -406,7 +456,7 @@ class MovePageForm {
} elseif( $this->moveTalk ) {
$conds = array(
'page_namespace' => $ot->getTalkPage()->getNamespace(),
- 'page_title' => $ot->getDBKey()
+ 'page_title' => $ot->getDBkey()
);
} else {
# Skip the query
@@ -428,15 +478,15 @@ class MovePageForm {
$skin = $wgUser->getSkin();
$count = 1;
foreach( $extraPages as $oldSubpage ) {
- if( $oldSubpage->getArticleId() == $ot->getArticleId() ) {
+ if( $ot->equals( $oldSubpage ) ) {
# Already did this one.
continue;
}
$newPageName = preg_replace(
- '#^'.preg_quote( $ot->getDBKey(), '#' ).'#',
- $nt->getDBKey(),
- $oldSubpage->getDBKey()
+ '#^'.preg_quote( $ot->getDBkey(), '#' ).'#',
+ StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
+ $oldSubpage->getDBkey()
);
if( $oldSubpage->isTalkPage() ) {
$newNs = $nt->getTalkPage()->getNamespace();
@@ -447,7 +497,7 @@ class MovePageForm {
# be longer than 255 characters.
$newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
if( !$newSubpage ) {
- $oldLink = $skin->makeKnownLinkObj( $oldSubpage );
+ $oldLink = $skin->linkKnown( $oldSubpage );
$extraOutput []= wfMsgHtml( 'movepage-page-unmoved', $oldLink,
htmlspecialchars(Title::makeName( $newNs, $newPageName )));
continue;
@@ -455,7 +505,7 @@ class MovePageForm {
# This was copy-pasted from Renameuser, bleh.
if ( $newSubpage->exists() && !$oldSubpage->isValidMoveTarget( $newSubpage ) ) {
- $link = $skin->makeKnownLinkObj( $newSubpage );
+ $link = $skin->linkKnown( $newSubpage );
$extraOutput []= wfMsgHtml( 'movepage-page-exists', $link );
} else {
$success = $oldSubpage->moveTo( $newSubpage, true, $this->reason, $createRedirect );
@@ -463,21 +513,26 @@ class MovePageForm {
if ( $this->fixRedirects ) {
DoubleRedirectJob::fixRedirects( 'move', $oldSubpage, $newSubpage );
}
- $oldLink = $skin->makeKnownLinkObj( $oldSubpage, '', 'redirect=no' );
- $newLink = $skin->makeKnownLinkObj( $newSubpage );
+ $oldLink = $skin->linkKnown(
+ $oldSubpage,
+ null,
+ array(),
+ array( 'redirect' => 'no' )
+ );
+ $newLink = $skin->linkKnown( $newSubpage );
$extraOutput []= wfMsgHtml( 'movepage-page-moved', $oldLink, $newLink );
+ ++$count;
+ if( $count >= $wgMaximumMovedPages ) {
+ $extraOutput []= wfMsgExt( 'movepage-max-pages', array( 'parsemag', 'escape' ), $wgLang->formatNum( $wgMaximumMovedPages ) );
+ break;
+ }
} else {
- $oldLink = $skin->makeKnownLinkObj( $oldSubpage );
- $newLink = $skin->makeLinkObj( $newSubpage );
+ $oldLink = $skin->linkKnown( $oldSubpage );
+ $newLink = $skin->link( $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() ) {
@@ -485,17 +540,24 @@ class MovePageForm {
}
# Deal with watches (we don't watch subpages)
- if( $this->watch ) {
+ if( $this->watch && $wgUser->isLoggedIn() ) {
$wgUser->addWatch( $ot );
$wgUser->addWatch( $nt );
} else {
$wgUser->removeWatch( $ot );
$wgUser->removeWatch( $nt );
}
+
+ # Re-clear the file redirect cache, which may have been polluted by
+ # parsing in messages above. See CR r56745.
+ # FIXME: needs a more robust solution inside FileRepo.
+ if( $ot->getNamespace() == NS_FILE ) {
+ RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $ot );
+ }
}
function showLogFragment( $title, &$out ) {
- $out->addHTML( Xml::element( 'h2', NULL, LogPage::logName( 'move' ) ) );
+ $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
index 575e37a7..a39b56ee 100644
--- a/includes/specials/SpecialNewimages.php
+++ b/includes/specials/SpecialNewimages.php
@@ -40,7 +40,8 @@ function wfSpecialNewimages( $par, $specialPage ) {
if ($hidebotsql) {
$sql .= "$hidebotsql WHERE ug_group IS NULL";
}
- $sql .= ' ORDER BY img_timestamp DESC LIMIT 1';
+ $sql .= ' ORDER BY img_timestamp DESC';
+ $sql = $dbr->limitResult($sql, 1, false);
$res = $dbr->query( $sql, __FUNCTION__ );
$row = $dbr->fetchRow( $res );
if( $row !== false ) {
@@ -64,13 +65,12 @@ function wfSpecialNewimages( $par, $specialPage ) {
}
$where = array();
- $searchpar = '';
+ $searchpar = array();
if ( $wpIlMatch != '' && !$wgMiserMode) {
- $nt = Title::newFromUrl( $wpIlMatch );
+ $nt = Title::newFromURL( $wpIlMatch );
if( $nt ) {
- $m = $dbr->escapeLike( strtolower( $nt->getDBkey() ) );
- $where[] = "LOWER(img_name) LIKE '%{$m}%'";
- $searchpar = '&wpIlMatch=' . urlencode( $wpIlMatch );
+ $where[] = 'LOWER(img_name) ' . $dbr->buildLike( $dbr->anyString(), strtolower( $nt->getDBkey() ), $dbr->anyString() );
+ $searchpar['wpIlMatch'] = $wpIlMatch;
}
}
@@ -93,7 +93,7 @@ function wfSpecialNewimages( $par, $specialPage ) {
$sql .= ' WHERE ' . $dbr->makeList( $where, LIST_AND );
}
$sql.=' ORDER BY img_timestamp '. ( $invertSort ? '' : ' DESC' );
- $sql.=' LIMIT ' . ( $limit + 1 );
+ $sql = $dbr->limitResult($sql, ( $limit + 1 ), false);
$res = $dbr->query( $sql, __FUNCTION__ );
/**
@@ -125,9 +125,9 @@ function wfSpecialNewimages( $par, $specialPage ) {
$ut = $s->img_user_text;
$nt = Title::newFromText( $name, NS_FILE );
- $ul = $sk->makeLinkObj( Title::makeTitle( NS_USER, $ut ), $ut );
+ $ul = $sk->link( Title::makeTitle( NS_USER, $ut ), $ut );
- $gallery->add( $nt, "$ul<br />\n<i>".$wgLang->timeanddate( $s->img_timestamp, true )."</i><br />\n" );
+ $gallery->add( $nt, "$ul<br />\n<i>".htmlspecialchars($wgLang->timeanddate( $s->img_timestamp, true ))."</i><br />\n" );
$timestamp = wfTimestamp( TS_MW, $s->img_timestamp );
if( empty( $firstTimestamp ) ) {
@@ -162,29 +162,72 @@ function wfSpecialNewimages( $par, $specialPage ) {
# If we change bot visibility, this needs to be carried along.
if( !$hidebots ) {
- $botpar = '&hidebots=0';
+ $botpar = array( 'hidebots' => 0 );
} else {
- $botpar = '';
+ $botpar = array();
}
$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);
-
+ $query = array_merge(
+ array( 'from' => $now ),
+ $botpar,
+ $searchpar
+ );
+
+ $dateLink = $sk->linkKnown(
+ $titleObj,
+ htmlspecialchars( wfMsg( 'sp-newimages-showfrom', $d, $t ) ),
+ array(),
+ $query
+ );
+
+ $query = array_merge(
+ array( 'hidebots' => ( $hidebots ? 0 : 1 ) ),
+ $searchpar
+ );
+
+ $showhide = $hidebots ? wfMsg( 'show' ) : wfMsg( 'hide' );
+
+ $botLink = $sk->linkKnown(
+ $titleObj,
+ htmlspecialchars( wfMsg( 'showhidebots', $showhide ) ),
+ array(),
+ $query
+ );
$opts = array( 'parsemag', 'escapenoentities' );
$prevLink = wfMsgExt( 'pager-newer-n', $opts, $wgLang->formatNum( $limit ) );
if( $firstTimestamp && $firstTimestamp != $latestTimestamp ) {
- $prevLink = $sk->makeKnownLinkObj( $titleObj, $prevLink, 'from=' . $firstTimestamp . $botpar . $searchpar );
+ $query = array_merge(
+ array( 'from' => $firstTimestamp ),
+ $botpar,
+ $searchpar
+ );
+
+ $prevLink = $sk->linkKnown(
+ $titleObj,
+ $prevLink,
+ array(),
+ $query
+ );
}
$nextLink = wfMsgExt( 'pager-older-n', $opts, $wgLang->formatNum( $limit ) );
if( $shownImages > $limit && $lastTimestamp ) {
- $nextLink = $sk->makeKnownLinkObj( $titleObj, $nextLink, 'until=' . $lastTimestamp.$botpar.$searchpar );
+ $query = array_merge(
+ array( 'until' => $lastTimestamp ),
+ $botpar,
+ $searchpar
+ );
+
+ $nextLink = $sk->linkKnown(
+ $titleObj,
+ $nextLink,
+ array(),
+ $query
+ );
+
}
$prevnext = '<p>' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'</p>';
diff --git a/includes/specials/SpecialNewpages.php b/includes/specials/SpecialNewpages.php
index 886c41a2..903ddab0 100644
--- a/includes/specials/SpecialNewpages.php
+++ b/includes/specials/SpecialNewpages.php
@@ -89,7 +89,7 @@ class SpecialNewpages extends SpecialPage {
* @return string
*/
public function execute( $par ) {
- global $wgLang, $wgUser, $wgOut;
+ global $wgLang, $wgOut;
$this->setHeaders();
$this->outputHeader();
@@ -165,6 +165,7 @@ class SpecialNewpages extends SpecialPage {
$this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
$namespace = $this->opts->consumeValue( 'namespace' );
$username = $this->opts->consumeValue( 'username' );
+ $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
// Check username input validity
$ut = Title::makeTitleSafe( NS_USER, $username );
@@ -177,7 +178,7 @@ class SpecialNewpages extends SpecialPage {
}
$hidden = implode( "\n", $hidden );
- $tagFilter = ChangeTags::buildTagFilterSelector( $this->opts['tagfilter'] );
+ $tagFilter = ChangeTags::buildTagFilterSelector( $tagFilterVal );
if ($tagFilter)
list( $tagFilterLabel, $tagFilterSelector ) = $tagFilter;
@@ -231,12 +232,8 @@ class SpecialNewpages extends SpecialPage {
protected function setSyndicated() {
global $wgOut;
- $queryParams = array(
- 'namespace' => $this->opts->getValue( 'namespace' ),
- 'username' => $this->opts->getValue( 'username' )
- );
$wgOut->setSyndicated( true );
- $wgOut->setFeedAppendQuery( wfArrayToCGI( $queryParams ) );
+ $wgOut->setFeedAppendQuery( wfArrayToCGI( $this->opts->getAllValues() ) );
}
/**
@@ -247,17 +244,32 @@ class SpecialNewpages extends SpecialPage {
* @return string
*/
public function formatRow( $result ) {
- global $wgLang, $wgContLang, $wgUser;
+ global $wgLang, $wgContLang;
$classes = array();
$dm = $wgContLang->getDirMark();
$title = Title::makeTitleSafe( $result->rc_namespace, $result->rc_title );
- $time = $wgLang->timeAndDate( $result->rc_timestamp, true );
- $query = $this->patrollable( $result ) ? "rcid={$result->rc_id}&redirect=no" : 'redirect=no';
- $plink = $this->skin->makeKnownLinkObj( $title, '', $query );
- $hist = $this->skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $time = htmlspecialchars( $wgLang->timeAndDate( $result->rc_timestamp, true ) );
+
+ $query = array( 'redirect' => 'no' );
+
+ if( $this->patrollable( $result ) )
+ $query['rcid'] = $result->rc_id;
+
+ $plink = $this->skin->linkKnown(
+ $title,
+ null,
+ array(),
+ $query
+ );
+ $hist = $this->skin->linkKnown(
+ $title,
+ wfMsgHtml( 'hist' ),
+ array(),
+ array( 'action' => 'history' )
+ );
$length = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ),
$wgLang->formatNum( $result->length ) );
$ulink = $this->skin->userLink( $result->rc_user, $result->rc_user_text ) . ' ' .
@@ -345,7 +357,7 @@ class SpecialNewpages extends SpecialPage {
$this->feedItemAuthor( $row ),
$comments);
} else {
- return NULL;
+ return null;
}
}
diff --git a/includes/specials/SpecialPopularpages.php b/includes/specials/SpecialPopularpages.php
index eb572736..88b90bc3 100644
--- a/includes/specials/SpecialPopularpages.php
+++ b/includes/specials/SpecialPopularpages.php
@@ -48,9 +48,15 @@ class PopularPagesPage extends QueryPage {
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 ) );
+ $link = $skin->linkKnown(
+ $title,
+ htmlspecialchars( $wgContLang->convert( $title->getPrefixedText() ) )
+ );
+ $nv = wfMsgExt(
+ 'nviews',
+ array( 'parsemag', 'escape'),
+ $wgLang->formatNum( $result->value )
+ );
return wfSpecialList($link, $nv);
}
}
diff --git a/includes/specials/SpecialPreferences.php b/includes/specials/SpecialPreferences.php
index 49c4f4e0..4c8bbb09 100644
--- a/includes/specials/SpecialPreferences.php
+++ b/includes/specials/SpecialPreferences.php
@@ -1,1308 +1,75 @@
<?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, $mStubs;
- var $mRows, $mCols, $mSkin, $mMath, $mDate, $mUserEmail, $mEmailFlag, $mNick;
- var $mUserLanguage, $mUserVariant;
- var $mSearch, $mRecent, $mRecentDays, $mTimeZone, $mHourDiff, $mSearchLines, $mSearchChars, $mAction;
- var $mReset, $mPosted, $mToggles, $mSearchNs, $mRealName, $mImageSize;
- var $mUnderline, $mWatchlistEdits, $mGender;
-
- /**
- * Constructor
- * Load some values
- */
- function __construct( &$request ) {
- global $wgContLang, $wgUser, $wgAllowRealName;
-
- $this->mQuickbar = $request->getVal( 'wpQuickbar' );
- $this->mStubs = $request->getVal( 'wpStubs' );
- $this->mRows = $request->getVal( 'wpRows' );
- $this->mCols = $request->getVal( 'wpCols' );
- $this->mSkin = Skin::normalizeKey( $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->mTimeZone = $request->getVal( 'wpTimeZone' );
- $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->mRestoreprefs = $request->getCheck( 'wpRestore' );
- $this->mPosted = $request->wasPosted();
- $this->mSuccess = $request->getCheck( 'success' );
- $this->mWatchlistDays = $request->getVal( 'wpWatchlistDays' );
- $this->mWatchlistEdits = $request->getVal( 'wpWatchlistEdits' );
- $this->mDisableMWSuggest = $request->getCheck( 'wpDisableMWSuggest' );
- $this->mGender = $request->getVal( 'wpGender' );
-
- $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 ) );
+class SpecialPreferences extends SpecialPage {
+ function __construct() {
+ parent::__construct( 'Preferences' );
}
- function execute() {
- global $wgUser, $wgOut, $wgTitle;
+ function execute( $par ) {
+ global $wgOut, $wgUser, $wgRequest;
+
+ $this->setHeaders();
+ $this->outputHeader();
+ $wgOut->disallowUserJs(); # Prevent hijacked user scripts from sniffing passwords etc.
if ( $wgUser->isAnon() ) {
- $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext', array($wgTitle->getPrefixedDBkey()) );
+ $wgOut->showErrorPage( 'prefsnologin', 'prefsnologintext', array( $this->getTitle()->getPrefixedDBkey() ) );
return;
}
if ( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
- if ( $this->mReset ) {
- $this->resetPrefs();
- $this->mainPrefsForm( 'reset', wfMsg( 'prefsreset' ) );
- } else if ( $this->mSaveprefs ) {
- $this->savePreferences();
- } else if ( $this->mRestoreprefs ) {
- $this->restorePreferences();
- } 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 'System' if fed bogus data.
- * @access private
- * @param string $tz the user input Zoneinfo timezone
- * @param string $s the user input offset string
- * @return string
- */
- function validateTimeZone( $tz, $s ) {
- $data = explode( '|', $tz, 3 );
- switch ( $data[0] ) {
- case 'ZoneInfo':
- case 'System':
- return $tz;
- case 'Offset':
- default:
- $data = explode( ':', $s, 2 );
- $minDiff = 0;
- if( count( $data ) == 2 ) {
- $data[0] = intval( $data[0] );
- $data[1] = intval( $data[1] );
- $minDiff = abs( $data[0] ) * 60 + $data[1];
- if ( $data[0] < 0 ) $minDiff = -$minDiff;
- } else {
- $minDiff = intval( $data[0] ) * 60;
- }
-
- # Max is +14:00 and min is -12:00, see:
- # http://en.wikipedia.org/wiki/Timezone
- $minDiff = min( $minDiff, 840 ); # 14:00
- $minDiff = max( $minDiff, -720 ); # -12:00
- return 'Offset|'.$minDiff;
- }
- }
-
- function validateGender( $val ) {
- $valid = array( 'male', 'female', 'unknown' );
- if ( in_array($val, $valid) ) {
- return $val;
- } else {
- return User::getDefaultOption( 'gender' );
- }
- }
-
- /**
- * @access private
- */
- function savePreferences() {
- global $wgUser, $wgOut, $wgParser;
- global $wgEnableUserEmail, $wgEnableEmail;
- global $wgEmailAuthentication, $wgRCMaxAge;
- global $wgAuth, $wgEmailConfirmToEdit;
-
- $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 ) ) );
+ if ( $par == 'reset' ) {
+ $this->showResetForm();
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 );
- global $wgAllowUserSkin;
- if( $wgAllowUserSkin ) {
- $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->mTimeZone, $this->mHourDiff ) );
- $wgUser->setOption( 'imagesize', $this->mImageSize );
- $wgUser->setOption( 'thumbsize', $this->mThumbSize );
- $wgUser->setOption( 'underline', $this->validateInt($this->mUnderline, 0, 2) );
- $wgUser->setOption( 'watchlistdays', $this->validateFloat( $this->mWatchlistDays, 0, 7 ) );
- $wgUser->setOption( 'disablesuggest', $this->mDisableMWSuggest );
- $wgUser->setOption( 'gender', $this->validateGender( $this->mGender ) );
-
- # 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 ) );
- }
}
+
+ $wgOut->addScriptFile( 'prefs.js' );
- if( !$wgAuth->updateExternalDB( $wgUser ) ){
- $this->mainPrefsForm( 'error', wfMsg( 'externaldberror' ) );
- return;
+ if ( $wgRequest->getCheck( 'success' ) ) {
+ $wgOut->wrapWikiMsg(
+ '<div class="successbox"><strong>$1</strong></div><div id="mw-pref-clear"></div>',
+ 'savedprefs'
+ );
}
-
- $msg = '';
- if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) {
- $this->mainPrefsForm( 'error', $msg );
- return;
+
+ if ( $wgRequest->getCheck( 'eauth' ) ) {
+ $wgOut->wrapWikiMsg( "<div class='error' style='clear: both;'>\n$1</div>",
+ 'eauthentsent', $wgUser->getName() );
}
- $wgUser->setCookies();
- $wgUser->saveSettings();
-
- if( $needRedirect && $error === false ) {
- $title = SpecialPage::getTitleFor( 'Preferences' );
- $wgOut->redirect( $title->getFullURL( 'success' ) );
- return;
- }
+ $htmlForm = Preferences::getFormObject( $wgUser );
+ $htmlForm->setSubmitCallback( array( 'Preferences', 'tryUISubmit' ) );
- $wgOut->parserOptions( ParserOptions::newFromUser( $wgUser ) );
- $this->mainPrefsForm( $error === false ? 'success' : 'error', $error);
+ $htmlForm->show();
}
- /**
- * @access private
- */
- function resetPrefs() {
- global $wgUser, $wgLang, $wgContLang, $wgContLanguageCode, $wgAllowRealName, $wgLocalTZoffset;
+ function showResetForm() {
+ global $wgOut;
- $this->mUserEmail = $wgUser->getEmail();
- $this->mUserEmailAuthenticationtimestamp = $wgUser->getEmailAuthenticationtimestamp();
- $this->mRealName = ($wgAllowRealName) ? $wgUser->getRealName() : '';
+ $wgOut->addWikiMsg( 'prefs-reset-intro' );
- # language value might be blank, default to content language
- $this->mUserLanguage = $wgUser->getOption( 'language', $wgContLanguageCode );
+ $htmlForm = new HTMLForm( array(), 'prefs-restore' );
- $this->mUserVariant = $wgUser->getOption( 'variant');
- $this->mEmailFlag = $wgUser->getOption( 'disablemail' ) == 1 ? 1 : 0;
- $this->mNick = $wgUser->getOption( 'nickname' );
+ $htmlForm->setSubmitText( wfMsg( 'restoreprefs' ) );
+ $htmlForm->setTitle( $this->getTitle( 'reset' ) );
+ $htmlForm->setSubmitCallback( array( __CLASS__, 'submitReset' ) );
+ $htmlForm->suppressReset();
- $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' );
-
- $tz = $wgUser->getOption( 'timecorrection' );
- $data = explode( '|', $tz, 3 );
- $minDiff = null;
- switch ( $data[0] ) {
- case 'ZoneInfo':
- $this->mTimeZone = $tz;
- # Check if the specified TZ exists, and change to 'Offset' if
- # not.
- if ( !function_exists('timezone_open') || @timezone_open( $data[2] ) === false ) {
- $this->mTimeZone = 'Offset';
- $minDiff = intval( $data[1] );
- }
- break;
- case '':
- case 'System':
- $this->mTimeZone = 'System|'.$wgLocalTZoffset;
- break;
- case 'Offset':
- $this->mTimeZone = 'Offset';
- $minDiff = intval( $data[1] );
- break;
- default:
- $this->mTimeZone = 'Offset';
- $data = explode( ':', $tz, 2 );
- if( count( $data ) == 2 ) {
- $data[0] = intval( $data[0] );
- $data[1] = intval( $data[1] );
- $minDiff = abs( $data[0] ) * 60 + $data[1];
- if ( $data[0] < 0 ) $minDiff = -$minDiff;
- } else {
- $minDiff = intval( $data[0] ) * 60;
- }
- break;
- }
- if ( is_null( $minDiff ) ) {
- $this->mHourDiff = '';
- } else {
- $this->mHourDiff = sprintf( '%+03d:%02d', floor($minDiff/60), abs($minDiff)%60 );
- }
-
- $this->mSearch = $wgUser->getOption( 'searchlimit' );
- $this->mSearchLines = $wgUser->getOption( 'contextlines' );
- $this->mSearchChars = $wgUser->getOption( 'contextchars' );
- $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->mDisableMWSuggest = $wgUser->getBoolOption( 'disablesuggest' );
- $this->mGender = $wgUser->getOption( 'gender' );
-
- $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 ) );
+ $htmlForm->show();
}
-
- /**
- * @access private
- */
- function restorePreferences() {
+
+ static function submitReset( $formData ) {
global $wgUser, $wgOut;
- $wgUser->restoreOptions();
- $wgUser->setCookies();
+ $wgUser->resetOptions();
$wgUser->saveSettings();
- $title = SpecialPage::getTitleFor( 'Preferences' );
- $wgOut->redirect( $title->getFullURL( 'success' ) );
- }
-
- /**
- * @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, $wgAuth;
- global $wgAllowRealName, $wgImageLimits, $wgThumbLimits;
- global $wgDisableLangConversion, $wgDisableTitleConversion;
- global $wgEnotifWatchlist, $wgEnotifUserTalk,$wgEnotifMinorEdits;
- global $wgRCShowWatchingUsers, $wgEnotifRevealEditorAddress;
- global $wgEnableEmail, $wgEnableUserEmail, $wgEmailAuthentication;
- global $wgContLanguageCode, $wgDefaultSkin, $wgCookieExpiration;
- global $wgEmailConfirmToEdit, $wgEnableMWSuggest, $wgLocalTZoffset;
-
- $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();
- $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;
- $this->mUsedToggles[ 'noconvertlink' ] = true;
-
-
- if ( !$this->mEmailFlag ) { $emfc = 'checked="checked"'; }
- else { $emfc = ''; }
-
-
- if ($wgEmailAuthentication && ($this->mUserEmail != '') ) {
- if( $wgUser->getEmailAuthenticationTimestamp() ) {
- // date and time are separate parameters to facilitate localisation.
- // $time is kept for backward compat reasons.
- // 'emailauthenticated' is also used in SpecialConfirmemail.php
- $time = $wgLang->timeAndDate( $wgUser->getEmailAuthenticationTimestamp(), true );
- $d = $wgLang->date( $wgUser->getEmailAuthenticationTimestamp(), true );
- $t = $wgLang->time( $wgUser->getEmailAuthenticationTimestamp(), true );
- $emailauthenticated = wfMsg('emailauthenticated', $time, $d, $t ).'<br />';
- $disableEmailPrefs = false;
- } else {
- $disableEmailPrefs = true;
- $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 ) ),
- $wgLang->commaList( $userEffectiveGroupsArray ) .
- '<br />(' . $wgLang->pipeList( $toolLinks ) . ')'
- ) .
-
- $this->tableRow(
- wfMsgHtml( 'prefs-edits' ),
- $wgLang->formatNum( $wgUser->getEditCount() )
- );
-
- 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' ) )
- );
-
- $gender = new XMLSelect( 'wpGender', 'wpGender', $this->mGender );
- $gender->addOption( wfMsg( 'gender-unknown' ), 'unknown' );
- $gender->addOption( wfMsg( 'gender-male' ), 'male' );
- $gender->addOption( wfMsg( 'gender-female' ), 'female' );
-
- $wgOut->addHTML(
- $this->tableRow(
- Xml::label( wfMsg( 'yourgender' ), 'wpGender' ),
- $gender->getHTML(),
- Xml::tags( 'div', array( 'class' => 'prefsectiontip' ),
- wfMsgExt( 'prefs-help-gender', 'parseinline' )
- )
- )
- );
-
- list( $lsLabel, $lsSelect) = Xml::languageSelector( $this->mUserLanguage, false );
- $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
- )
- )
- );
- }
-
- if(count($variantArray) > 1 && !$wgDisableLangConversion && !$wgDisableTitleConversion) {
- $wgOut->addHTML(
- Xml::tags( 'tr', null,
- Xml::tags( 'td', array( 'colspan' => '2' ),
- $this->getToggle( "noconvertlink" )
- )
- )
- );
- }
- }
-
- # Password
- if( $wgAuth->allowPasswordChange() ) {
- $link = $wgUser->getSkin()->link( SpecialPage::getTitleFor( 'ResetPass' ), wfMsgHtml( 'prefs-resetpass' ),
- array() , array( 'returnto' => SpecialPage::getTitleFor( 'Preferences' )->getPrefixedText() ) );
- $wgOut->addHTML(
- $this->tableRow( Xml::element( 'h2', null, wfMsg( 'changepassword' ) ) ) .
- $this->tableRow( '<ul><li>' . $link . '</li></ul>' ) );
- }
-
- # <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( Xml::hidden( 'wpQuickbar', $this->mQuickbar ) );
- }
-
- # Skin
- #
- global $wgAllowUserSkin;
- if( $wgAllowUserSkin ) {
- $wgOut->addHTML( "<fieldset>\n<legend>\n" . wfMsg( 'skin' ) . "</legend>\n" );
- $mptitle = Title::newMainPage();
- $previewtext = wfMsg( 'skin-preview' );
- # Only show members of Skin::getSkinNames() rather than
- # $skinNames (skins is all skin names from Language.php)
- $validSkinNames = Skin::getUsableSkins();
- # Sort by UI skin name. First though need to update validSkinNames as sometimes
- # the skinkey & UI skinname differ (e.g. "standard" skinkey is "Classic" in the UI).
- foreach ( $validSkinNames as $skinkey => &$skinname ) {
- $msgName = "skinname-{$skinkey}";
- $localisedSkinName = wfMsg( $msgName );
- if ( !wfEmptyMsg( $msgName, $localisedSkinName ) ) {
- $skinname = $localisedSkinName;
- }
- }
- asort($validSkinNames);
- foreach( $validSkinNames as $skinkey => $sn ) {
- $checked = $skinkey == $this->mSkin ? ' checked="checked"' : '';
- $mplink = htmlspecialchars( $mptitle->getLocalURL( "useskin=$skinkey" ) );
- $previewlink = "(<a target='_blank' href=\"$mplink\">$previewtext</a>)";
- $extraLinks = '';
- global $wgAllowUserCss, $wgAllowUserJs;
- if( $wgAllowUserCss ) {
- $cssPage = Title::makeTitleSafe( NS_USER, $wgUser->getName().'/'.$skinkey.'.css' );
- $customCSS = $sk->makeLinkObj( $cssPage, wfMsgExt('prefs-custom-css', array() ) );
- $extraLinks .= " ($customCSS)";
- }
- if( $wgAllowUserJs ) {
- $jsPage = Title::makeTitleSafe( NS_USER, $wgUser->getName().'/'.$skinkey.'.js' );
- $customJS = $sk->makeLinkObj( $jsPage, wfMsgHtml('prefs-custom-js') );
- $extraLinks .= " ($customJS)";
- }
- 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{$extraLinks}<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
- #
- $imageLimitOptions = null;
- foreach ( $wgImageLimits as $index => $limits ) {
- $selected = ($index == $this->mImageSize);
- $imageLimitOptions .= Xml::option( "{$limits[0]}×{$limits[1]}" .
- wfMsg('unit-pixel'), $index, $selected );
- }
-
- $imageThumbOptions = null;
- foreach ( $wgThumbLimits as $index => $size ) {
- $selected = ($index == $this->mThumbSize);
- $imageThumbOptions .= Xml::option($size . wfMsg('unit-pixel'), $index,
- $selected);
- }
-
- $imageSizeId = 'wpImageSize';
- $thumbSizeId = 'wpThumbSize';
- $wgOut->addHTML(
- Xml::fieldset( wfMsg( 'files' ) ) . "\n" .
- Xml::openElement( 'table' ) .
- '<tr>
- <td class="mw-label">' .
- Xml::label( wfMsg( 'imagemaxsize' ), $imageSizeId ) .
- '</td>
- <td class="mw-input">' .
- Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) .
- $imageLimitOptions .
- Xml::closeElement( 'select' ) .
- '</td>
- </tr><tr>
- <td class="mw-label">' .
- Xml::label( wfMsg( 'thumbsize' ), $thumbSizeId ) .
- '</td>
- <td class="mw-input">' .
- Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) .
- $imageThumbOptions .
- Xml::closeElement( 'select' ) .
- '</td>
- </tr>' .
- Xml::closeElement( 'table' ) .
- Xml::closeElement( 'fieldset' )
- );
-
- # 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 = Xml::openElement( 'span', array( 'id' => 'wpLocalTime' ) ) .
- $wgLang->time( $now = wfTimestampNow(), true ) .
- Xml::closeElement( 'span' );
- $nowserver = $wgLang->time( $now, false ) .
- Xml::hidden( 'wpServerTime', substr( $now, 8, 2 ) * 60 + substr( $now, 10, 2 ) );
-
- $wgOut->addHTML(
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'timezonelegend' ) ) .
- Xml::openElement( 'table' ) .
- $this->addRow( wfMsg( 'servertime' ), $nowserver ) .
- $this->addRow( wfMsg( 'localtime' ), $nowlocal )
- );
- $opt = Xml::openElement( 'select', array(
- 'name' => 'wpTimeZone',
- 'id' => 'wpTimeZone',
- 'onchange' => 'javascript:updateTimezoneSelection(false)' ) );
- $opt .= Xml::option( wfMsg( 'timezoneuseserverdefault' ), "System|$wgLocalTZoffset", $this->mTimeZone === "System|$wgLocalTZoffset" );
- $opt .= Xml::option( wfMsg( 'timezoneuseoffset' ), 'Offset', $this->mTimeZone === 'Offset' );
-
- if ( function_exists( 'timezone_identifiers_list' ) ) {
- # Read timezone list
- $tzs = timezone_identifiers_list();
- sort( $tzs );
-
- # Precache localized region names
- $tzRegions = array();
- $tzRegions['Africa'] = wfMsg( 'timezoneregion-africa' );
- $tzRegions['America'] = wfMsg( 'timezoneregion-america' );
- $tzRegions['Antarctica'] = wfMsg( 'timezoneregion-antarctica' );
- $tzRegions['Arctic'] = wfMsg( 'timezoneregion-arctic' );
- $tzRegions['Asia'] = wfMsg( 'timezoneregion-asia' );
- $tzRegions['Atlantic'] = wfMsg( 'timezoneregion-atlantic' );
- $tzRegions['Australia'] = wfMsg( 'timezoneregion-australia' );
- $tzRegions['Europe'] = wfMsg( 'timezoneregion-europe' );
- $tzRegions['Indian'] = wfMsg( 'timezoneregion-indian' );
- $tzRegions['Pacific'] = wfMsg( 'timezoneregion-pacific' );
- asort( $tzRegions );
-
- $selZone = explode( '|', $this->mTimeZone, 3 );
- $selZone = ( $selZone[0] == 'ZoneInfo' ) ? $selZone[2] : null;
- $now = date_create( 'now' );
- $optgroup = '';
-
- foreach ( $tzs as $tz ) {
- $z = explode( '/', $tz, 2 );
-
- # timezone_identifiers_list() returns a number of
- # backwards-compatibility entries. This filters them out of the
- # list presented to the user.
- if ( count( $z ) != 2 || !array_key_exists( $z[0], $tzRegions ) )
- continue;
-
- # Localize region
- $z[0] = $tzRegions[$z[0]];
-
- # Create region groups
- if ( $optgroup != $z[0] ) {
- if ( $optgroup !== '' ) {
- $opt .= Xml::closeElement( 'optgroup' );
- }
- $optgroup = $z[0];
- $opt .= Xml::openElement( 'optgroup', array( 'label' => $z[0] ) ) . "\n";
- }
-
- $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 );
- $opt .= Xml::option( str_replace( '_', ' ', $z[0] . '/' . $z[1] ), "ZoneInfo|$minDiff|$tz", $selZone === $tz, array( 'label' => $z[1] ) ) . "\n";
- }
- if ( $optgroup !== '' ) $opt .= Xml::closeElement( 'optgroup' );
- }
- $opt .= Xml::closeElement( 'select' );
- $wgOut->addHTML(
- $this->addRow(
- Xml::label( wfMsg( 'timezoneselect' ), 'wpTimeZone' ),
- $opt )
- );
- $wgOut->addHTML(
- $this->addRow(
- Xml::label( wfMsg( 'timezoneoffset' ), 'wpHourDiff' ),
- Xml::input( 'wpHourDiff', 6, $this->mHourDiff, array(
- 'id' => 'wpHourDiff',
- 'onfocus' => 'javascript:updateTimezoneSelection(true)',
- 'onblur' => 'javascript:updateTimezoneSelection(false)' ) ) ) .
- "<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(
- Xml::fieldset( wfMsg( 'textboxsize' ) ) .
- wfMsgHTML( 'prefs-edit-boxsize' ) . ' ' .
- Xml::inputLabel( wfMsg( 'rows' ), 'wpRows', 'wpRows', 3, $this->mRows ) . ' ' .
- Xml::inputLabel( wfMsg( 'columns' ), 'wpCols', 'wpCols', 3, $this->mCols ) .
- $this->getToggles( array(
- 'editsection',
- 'editsectiononrightclick',
- 'editondblclick',
- 'editwidth',
- 'showtoolbar',
- 'previewonfirst',
- 'previewontop',
- 'minordefault',
- 'externaleditor',
- 'externaldiff',
- $wgLivePreview ? 'uselivepreview' : false,
- 'forceeditsummary',
- ) )
- );
-
- $wgOut->addHTML( Xml::closeElement( 'fieldset' ) );
-
- # Recent changes
- global $wgRCMaxAge, $wgUseRCPatrol;
- $wgOut->addHTML(
- Xml::fieldset( wfMsg( 'prefs-rc' ) ) .
- Xml::openElement( 'table' ) .
- '<tr>
- <td class="mw-label">' .
- Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) .
- '</td>
- <td class="mw-input">' .
- Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . ' ' .
- wfMsgExt( 'recentchangesdays-max', 'parsemag',
- $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) ) .
- '</td>
- </tr><tr>
- <td class="mw-label">' .
- Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) .
- '</td>
- <td class="mw-input">' .
- Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) .
- '</td>
- </tr>' .
- Xml::closeElement( 'table' ) .
- '<br />'
- );
-
- $toggles[] = 'hideminor';
- if( $wgUseRCPatrol ) {
- $toggles[] = 'hidepatrolled';
- $toggles[] = 'newpageshidepatrolled';
- }
- if( $wgRCShowWatchingUsers ) $toggles[] = 'shownumberswatching';
- $toggles[] = 'usenewrc';
-
- $wgOut->addHTML(
- $this->getToggles( $toggles ) .
- Xml::closeElement( 'fieldset' )
- );
-
- # Watchlist
- $watchlistToggles = array( 'watchlisthideminor', 'watchlisthidebots', 'watchlisthideown',
- 'watchlisthideanons', 'watchlisthideliu' );
- if( $wgUseRCPatrol ) $watchlistToggles[] = 'watchlisthidepatrolled';
-
- $wgOut->addHTML(
- Xml::fieldset( wfMsg( 'prefs-watchlist' ) ) .
- Xml::inputLabel( wfMsg( 'prefs-watchlist-days' ), 'wpWatchlistDays', 'wpWatchlistDays', 3, $this->mWatchlistDays ) . ' ' .
- wfMsgHTML( 'prefs-watchlist-days-max' ) .
- '<br /><br />' .
- $this->getToggle( 'extendwatchlist' ) .
- Xml::inputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) . ' ' .
- wfMsgHTML( 'prefs-watchlist-edits-max' ) .
- '<br /><br />' .
- $this->getToggles( $watchlistToggles )
- );
-
- 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( Xml::closeElement( 'fieldset' ) );
-
- # Search
- $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' ) .
- $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
- #
- $uopt = $wgUser->getOption( 'underline' );
- $wgOut->addHTML(
- Xml::fieldset( wfMsg( 'prefs-misc' ) ) .
- Xml::openElement( 'table' ) .
- '<tr>
- <td class="mw-label">' .
- // Xml::label() cannot be used because 'stub-threshold' contains plain HTML
- Xml::tags( 'label', array( 'for' => 'wpStubs' ), wfMsg( 'stub-threshold' ) ) .
- '</td>
- <td class="mw-input">' .
- Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) .
- '</td>
- </tr><tr>
- <td class="mw-label">' .
- Xml::label( wfMsg( 'tog-underline' ), 'wpOpunderline' ) .
- '</td>
- <td class="mw-input">' .
- Xml::openElement( 'select', array( 'id' => 'wpOpunderline', 'name' => 'wpOpunderline' ) ) .
- Xml::option( wfMsg ( 'underline-never' ), '0', $uopt == 0 ) .
- Xml::option( wfMsg ( 'underline-always' ), '1', $uopt == 1 ) .
- Xml::option( wfMsg ( 'underline-default' ), '2', $uopt == 2 ) .
- Xml::closeElement( 'select' ) .
- '</td>
- </tr>' .
- Xml::closeElement( 'table' )
- );
-
- # And now the rest = Misc.
- foreach ( $togs as $tname ) {
- if( !array_key_exists( $tname, $this->mUsedToggles ) ) {
- if( $tname == 'norollbackdiff' && $wgUser->isAllowed( 'rollback' ) )
- $wgOut->addHTML( $this->getToggle( $tname ) );
- else
- $wgOut->addHTML( $this->getToggle( $tname ) );
- }
- }
-
- $wgOut->addHTML( '</fieldset>' );
-
- wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) );
- $token = htmlspecialchars( $wgUser->editToken() );
- $skin = $wgUser->getSkin();
- $rtl = $wgContLang->isRTL() ? 'left' : 'right';
- $wgOut->addHTML( "
- <table id='prefsubmit' cellpadding='0' width='100%' style='background:none;'><tr>
- <td><input type='submit' name='wpSaveprefs' class='btnSavePrefs' value=\"" . wfMsgHtml( 'saveprefs' ) .
- '"'.$skin->tooltipAndAccesskey('save')." />
- <input type='submit' name='wpReset' value=\"" . wfMsgHtml( 'resetprefs' ) . "\" /></td>
- <td align='$rtl'><input type='submit' name='wpRestore' value=\"" . wfMsgHtml( 'restoreprefs' ) . "\" /></td>
- </tr></table>
+ $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( 'success' );
- <input type='hidden' name='wpEditToken' value=\"{$token}\" />
- </div></form>\n" );
+ $wgOut->redirect( $url );
- $wgOut->addHTML( Xml::tags( 'div', array( 'class' => "prefcache" ),
- wfMsgExt( 'clearyourcache', 'parseinline' ) )
- );
+ return true;
}
}
diff --git a/includes/specials/SpecialPrefixindex.php b/includes/specials/SpecialPrefixindex.php
index 680fe343..8b5f0c93 100644
--- a/includes/specials/SpecialPrefixindex.php
+++ b/includes/specials/SpecialPrefixindex.php
@@ -63,7 +63,7 @@ class SpecialPrefixindex extends SpecialAllpages {
Xml::label( wfMsg( 'allpagesprefix' ), 'nsfrom' ) .
"</td>
<td class='mw-input'>" .
- Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
+ Xml::input( 'prefix', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) .
"</td>
</tr>
<tr>
@@ -115,7 +115,7 @@ class SpecialPrefixindex extends SpecialAllpages {
array( 'page_namespace', 'page_title', 'page_is_redirect' ),
array(
'page_namespace' => $namespace,
- 'page_title LIKE \'' . $dbr->escapeLike( $prefixKey ) .'%\'',
+ 'page_title' . $dbr->buildLike( $prefixKey, $dbr->anyString() ),
'page_title >= ' . $dbr->addQuotes( $fromKey ),
),
__METHOD__,
@@ -136,7 +136,10 @@ class SpecialPrefixindex extends SpecialAllpages {
$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 ) .
+ $sk->linkKnown(
+ $t,
+ htmlspecialchars( $t->getText() )
+ ) .
($s->page_is_redirect ? '</div>' : '' );
} else {
$link = '[[' . htmlspecialchars( $s->page_title ) . ']]';
@@ -170,17 +173,26 @@ class SpecialPrefixindex extends SpecialAllpages {
$nsForm .
'</td>
<td id="mw-prefixindex-nav-form">' .
- $sk->makeKnownLinkObj( $self, wfMsg ( 'allpages' ) );
+ $sk->linkKnown( $self, wfMsgHtml( 'allpages' ) );
if( isset( $res ) && $res && ( $n == $this->maxPerPage ) && ( $s = $res->fetchObject() ) ) {
- $namespaceparam = $namespace ? "&namespace=$namespace" : "";
+ $query = array(
+ 'from' => $s->page_title,
+ 'prefix' => $prefix
+ );
+
+ if( $namespace ) {
+ $query['namespace'] = $namespace;
+ }
+
$out2 = $wgLang->pipeList( array(
$out2,
- $sk->makeKnownLinkObj(
+ $sk->linkKnown(
$self,
wfMsgHtml( 'nextpage', str_replace( '_',' ', htmlspecialchars( $s->page_title ) ) ),
- "from=" . wfUrlEncode( $s->page_title ) .
- "&prefix=" . wfUrlEncode( $prefix ) . $namespaceparam )
+ array(),
+ $query
+ )
) );
}
$out2 .= "</td></tr>" .
diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php
index a38a8cd1..8229770c 100644
--- a/includes/specials/SpecialProtectedpages.php
+++ b/includes/specials/SpecialProtectedpages.php
@@ -16,7 +16,7 @@ class ProtectedPagesForm {
public function showList( $msg = '' ) {
global $wgOut, $wgRequest;
- if( "" != $msg ) {
+ if( $msg != "" ) {
$wgOut->setSubtitle( $msg );
}
@@ -65,7 +65,7 @@ class ProtectedPagesForm {
$skin = $wgUser->getSkin();
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
- $link = $skin->makeLinkObj( $title );
+ $link = $skin->link( $title );
$description_items = array ();
@@ -86,7 +86,7 @@ class ProtectedPagesForm {
$expiry_description = wfMsg( 'protect-expiring' , $wgLang->timeanddate( $expiry ) ,
$wgLang->date( $expiry ) , $wgLang->time( $expiry ) );
- $description_items[] = $expiry_description;
+ $description_items[] = htmlspecialchars($expiry_description);
}
if(!is_null($size = $row->page_len)) {
@@ -95,17 +95,31 @@ class ProtectedPagesForm {
# 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' ) . ')';
+ $changeProtection = ' (' . $skin->linkKnown(
+ $title,
+ wfMsgHtml( 'protect_change' ),
+ array(),
+ array( 'action' => 'unprotect' )
+ ) . ')';
} else {
$ltitle = SpecialPage::getTitleFor( 'Log' );
- $changeProtection = ' (' . $skin->makeKnownLinkObj( $ltitle, wfMsgHtml( 'protectlogpage' ),
- 'type=protect&page=' . $title->getPrefixedUrl() ) . ')';
+ $changeProtection = ' (' . $skin->linkKnown(
+ $ltitle,
+ wfMsgHtml( 'protectlogpage' ),
+ array(),
+ array(
+ 'type' => 'protect',
+ 'page' => $title->getPrefixedText()
+ )
+ ) . ')';
}
wfProfileOut( __METHOD__ );
- return '<li>' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "</li>\n";
+ return Html::rawElement(
+ 'li',
+ array(),
+ wfSpecialList( $link . $stxt, $wgLang->commaList( $description_items ) ) . $changeProtection ) . "\n";
}
/**
@@ -120,7 +134,7 @@ class ProtectedPagesForm {
*/
protected function showOptions( $namespace, $type='edit', $level, $sizetype, $size, $indefOnly, $cascadeOnly ) {
global $wgScript;
- $title = SpecialPage::getTitleFor( 'ProtectedPages' );
+ $title = SpecialPage::getTitleFor( 'Protectedpages' );
return Xml::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript ) ) .
Xml::openElement( 'fieldset' ) .
Xml::element( 'legend', array(), wfMsg( 'protectedpages' ) ) .
@@ -128,10 +142,10 @@ class ProtectedPagesForm {
$this->getNamespaceMenu( $namespace ) . "&nbsp;\n" .
$this->getTypeMenu( $type ) . "&nbsp;\n" .
$this->getLevelMenu( $level ) . "&nbsp;\n" .
- "<br/><span style='white-space: nowrap'>" .
+ "<br /><span style='white-space: nowrap'>" .
$this->getExpiryCheck( $indefOnly ) . "&nbsp;\n" .
$this->getCascadeCheck( $cascadeOnly ) . "&nbsp;\n" .
- "</span><br/><span style='white-space: nowrap'>" .
+ "</span><br /><span style='white-space: nowrap'>" .
$this->getSizeLimit( $sizetype, $size ) . "&nbsp;\n" .
"</span>" .
"&nbsp;" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
@@ -185,6 +199,8 @@ class ProtectedPagesForm {
}
/**
+ * Creates the input label of the restriction type
+ * @param $pr_type string Protection type
* @return string Formatted HTML
*/
protected function getTypeMenu( $pr_type ) {
@@ -213,6 +229,8 @@ class ProtectedPagesForm {
}
/**
+ * Creates the input label of the restriction level
+ * @param $pr_level string Protection level
* @return string Formatted HTML
*/
protected function getLevelMenu( $pr_level ) {
@@ -223,6 +241,7 @@ class ProtectedPagesForm {
// First pass to load the log names
foreach( $wgRestrictionLevels as $type ) {
+ // Messages used can be 'restriction-level-sysop' and 'restriction-level-autoconfirmed'
if( $type !='' && $type !='*') {
$text = wfMsg("restriction-level-$type");
$m[$text] = $type;
@@ -235,11 +254,11 @@ class ProtectedPagesForm {
$options[] = Xml::option( $text, $type, $selected );
}
- return
- Xml::label( wfMsg('restriction-level') , $this->IdLevel ) . '&nbsp;' .
+ return "<span style='white-space: nowrap'>" .
+ Xml::label( wfMsg( 'restriction-level' ) , $this->IdLevel ) . ' ' .
Xml::tags( 'select',
array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ),
- implode( "\n", $options ) );
+ implode( "\n", $options ) ) . "</span>";
}
}
diff --git a/includes/specials/SpecialProtectedtitles.php b/includes/specials/SpecialProtectedtitles.php
index 7e8126d9..d65b3f79 100644
--- a/includes/specials/SpecialProtectedtitles.php
+++ b/includes/specials/SpecialProtectedtitles.php
@@ -16,7 +16,7 @@ class ProtectedTitlesForm {
function showList( $msg = '' ) {
global $wgOut, $wgRequest;
- if ( "" != $msg ) {
+ if ( $msg != "" ) {
$wgOut->setSubtitle( $msg );
}
@@ -61,7 +61,7 @@ class ProtectedTitlesForm {
$skin = $wgUser->getSkin();
$title = Title::makeTitleSafe( $row->pt_namespace, $row->pt_title );
- $link = $skin->makeLinkObj( $title );
+ $link = $skin->link( $title );
$description_items = array ();
@@ -94,7 +94,7 @@ class ProtectedTitlesForm {
function showOptions( $namespace, $type='edit', $level, $sizetype, $size ) {
global $wgScript;
$action = htmlspecialchars( $wgScript );
- $title = SpecialPage::getTitleFor( 'ProtectedTitles' );
+ $title = SpecialPage::getTitleFor( 'Protectedtitles' );
$special = htmlspecialchars( $title->getPrefixedDBkey() );
return "<form action=\"$action\" method=\"get\">\n" .
'<fieldset>' .
diff --git a/includes/specials/SpecialRandompage.php b/includes/specials/SpecialRandompage.php
index 31199b23..fd3f17f2 100644
--- a/includes/specials/SpecialRandompage.php
+++ b/includes/specials/SpecialRandompage.php
@@ -9,12 +9,12 @@
*/
class RandomPage extends SpecialPage {
private $namespaces; // namespaces to select pages from
+ protected $isRedir = false; // should the result be a redirect?
+ protected $extra = array(); // Extra SQL statements
- function __construct( $name = 'Randompage' ){
+ public function __construct( $name = 'Randompage' ){
global $wgContentNamespaces;
-
$this->namespaces = $wgContentNamespaces;
-
parent::__construct( $name );
}
@@ -28,22 +28,23 @@ class RandomPage extends SpecialPage {
}
// select redirects instead of normal pages?
- // Overriden by SpecialRandomredirect
public function isRedirect(){
- return false;
+ return $this->isRedir;
}
public function execute( $par ) {
global $wgOut, $wgContLang;
- if ($par)
+ if ($par) {
$this->setNamespace( $wgContLang->getNsIndex( $par ) );
+ }
$title = $this->getRandomTitle();
if( is_null( $title ) ) {
$this->setHeaders();
- $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages', $wgContLang->getNsText( $this->namespace ) );
+ $wgOut->addWikiMsg( strtolower( $this->mName ) . '-nopages',
+ $this->getNsList(), count( $this->namespaces ) );
return;
}
@@ -51,6 +52,23 @@ class RandomPage extends SpecialPage {
$wgOut->redirect( $title->getFullUrl( $query ) );
}
+ /**
+ * Get a comma-delimited list of namespaces we don't have
+ * any pages in
+ * @return String
+ */
+ private function getNsList() {
+ global $wgContLang;
+ $nsNames = array();
+ foreach( $this->namespaces as $n ) {
+ if( $n === NS_MAIN )
+ $nsNames[] = wfMsgForContent( 'blanknamespace' );
+ else
+ $nsNames[] = $wgContLang->getNsText( $n );
+ }
+ return $wgContLang->commaList( $nsNames );
+ }
+
/**
* Choose a random title.
@@ -58,6 +76,10 @@ class RandomPage extends SpecialPage {
*/
public function getRandomTitle() {
$randstr = wfRandom();
+ $title = null;
+ if ( !wfRunHooks( 'SpecialRandomGetRandomTitle', array( &$randstr, &$this->isRedir, &$this->namespaces, &$this->extra, &$title ) ) ) {
+ return $title;
+ }
$row = $this->selectRandomPageFromDB( $randstr );
/* If we picked a value that was higher than any in
@@ -78,8 +100,6 @@ class RandomPage extends SpecialPage {
private function selectRandomPageFromDB( $randstr ) {
global $wgExtraRandompageSQL;
- $fname = 'RandomPage::selectRandomPageFromDB';
-
$dbr = wfGetDB( DB_SLAVE );
$use_index = $dbr->useIndexClause( 'page_random' );
@@ -87,8 +107,17 @@ class RandomPage extends SpecialPage {
$ns = implode( ",", $this->namespaces );
$redirect = $this->isRedirect() ? 1 : 0;
-
- $extra = $wgExtraRandompageSQL ? "AND ($wgExtraRandompageSQL)" : "";
+
+ if ( $wgExtraRandompageSQL ) {
+ $this->extra[] = $wgExtraRandompageSQL;
+ }
+ if ( $this->addExtraSQL() ) {
+ $this->extra[] = $this->addExtraSQL();
+ }
+ $extra = '';
+ if ( $this->extra ) {
+ $extra = 'AND (' . implode( ') AND (', $this->extra ) . ')';
+ }
$sql = "SELECT page_title, page_namespace
FROM $page $use_index
WHERE page_namespace IN ( $ns )
@@ -98,7 +127,15 @@ class RandomPage extends SpecialPage {
ORDER BY page_random";
$sql = $dbr->limitResult( $sql, 1, 0 );
- $res = $dbr->query( $sql, $fname );
+ $res = $dbr->query( $sql, __METHOD__ );
return $dbr->fetchObject( $res );
}
+
+ /* an alternative to $wgExtraRandompageSQL so subclasses
+ * can add their own SQL by overriding this function
+ * @deprecated, append to $this->extra instead
+ */
+ public function addExtraSQL() {
+ return '';
+ }
}
diff --git a/includes/specials/SpecialRandomredirect.php b/includes/specials/SpecialRandomredirect.php
index 629d5b3c..28cb2aae 100644
--- a/includes/specials/SpecialRandomredirect.php
+++ b/includes/specials/SpecialRandomredirect.php
@@ -10,10 +10,7 @@
class SpecialRandomredirect extends RandomPage {
function __construct(){
parent::__construct( 'Randomredirect' );
+ $this->isRedir = true;
}
- // Override parent::isRedirect()
- public function isRedirect(){
- return true;
- }
}
diff --git a/includes/specials/SpecialRecentchanges.php b/includes/specials/SpecialRecentchanges.php
index 91c0ecbe..283eeaf4 100644
--- a/includes/specials/SpecialRecentchanges.php
+++ b/includes/specials/SpecialRecentchanges.php
@@ -5,6 +5,8 @@
* @ingroup SpecialPage
*/
class SpecialRecentChanges extends SpecialPage {
+ var $rcOptions, $rcSubpage;
+
public function __construct() {
parent::__construct( 'Recentchanges' );
$this->includable( true );
@@ -40,7 +42,7 @@ class SpecialRecentChanges extends SpecialPage {
}
/**
- * Get a FormOptions object with options as specified by the user
+ * Create a FormOptions object with options as specified by the user
*
* @return FormOptions
*/
@@ -55,31 +57,45 @@ class SpecialRecentChanges extends SpecialPage {
$this->parseParameters( $parameters, $opts );
}
- $opts->validateIntBounds( 'limit', 0, 500 );
+ $opts->validateIntBounds( 'limit', 0, 5000 );
return $opts;
}
/**
- * Get a FormOptions object sepcific for feed requests
+ * Create a FormOptions object specific for feed requests and return it
*
* @return FormOptions
*/
public function feedSetup() {
global $wgFeedLimit, $wgRequest;
$opts = $this->getDefaultOptions();
- # Feed is cached on limit,hideminor; other params would randomly not work
- $opts->fetchValuesFromRequest( $wgRequest, array( 'limit', 'hideminor' ) );
+ # Feed is cached on limit,hideminor,namespace; other params would randomly not work
+ $opts->fetchValuesFromRequest( $wgRequest, array( 'limit', 'hideminor', 'namespace' ) );
$opts->validateIntBounds( 'limit', 0, $wgFeedLimit );
return $opts;
}
/**
+ * Get the current FormOptions for this request
+ */
+ public function getOptions() {
+ if ( $this->rcOptions === null ) {
+ global $wgRequest;
+ $feedFormat = $wgRequest->getVal( 'feed' );
+ $this->rcOptions = $feedFormat ? $this->feedSetup() : $this->setup( $this->rcSubpage );
+ }
+ return $this->rcOptions;
+ }
+
+
+ /**
* Main execution point
*
- * @param $parameters string
+ * @param $subpage string
*/
- public function execute( $parameters ) {
+ public function execute( $subpage ) {
global $wgRequest, $wgOut;
+ $this->rcSubpage = $subpage;
$feedFormat = $wgRequest->getVal( 'feed' );
# 10 seconds server-side caching max
@@ -90,12 +106,11 @@ class SpecialRecentChanges extends SpecialPage {
return;
}
- $opts = $feedFormat ? $this->feedSetup() : $this->setup( $parameters );
+ $opts = $this->getOptions();
$this->setHeaders();
$this->outputHeader();
// Fetch results, prepare a batch link existence check query
- $rows = array();
$conds = $this->buildMainQueryConds( $opts );
$rows = $this->doMainQuery( $conds, $opts );
if( $rows === false ){
@@ -114,10 +129,9 @@ class SpecialRecentChanges extends SpecialPage {
}
$batch->execute();
}
- $target = isset($opts['target']) ? $opts['target'] : ''; // RCL has targets
if( $feedFormat ) {
- list( $feed, $feedObj ) = $this->getFeedObject( $feedFormat );
- $feed->execute( $feedObj, $rows, $opts['limit'], $opts['hideminor'], $lastmod, $target );
+ list( $changesFeed, $formatter ) = $this->getFeedObject( $feedFormat );
+ $changesFeed->execute( $formatter, $rows, $lastmod, $opts );
} else {
$this->webOutput( $rows, $opts );
}
@@ -131,12 +145,12 @@ class SpecialRecentChanges extends SpecialPage {
* @return array
*/
public function getFeedObject( $feedFormat ){
- $feed = new ChangesFeed( $feedFormat, 'rcfeed' );
- $feedObj = $feed->getFeedObject(
+ $changesFeed = new ChangesFeed( $feedFormat, 'rcfeed' );
+ $formatter = $changesFeed->getFeedObject(
wfMsgForContent( 'recentchanges' ),
wfMsgForContent( 'recentchanges-feed-description' )
);
- return array( $feed, $feedObj );
+ return array( $changesFeed, $formatter );
}
/**
@@ -177,7 +191,7 @@ class SpecialRecentChanges extends SpecialPage {
public function checkLastModified( $feedFormat ) {
global $wgUseRCPatrol, $wgOut;
$dbr = wfGetDB( DB_SLAVE );
- $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __FUNCTION__ );
+ $lastmod = $dbr->selectField( 'recentchanges', 'MAX(rc_timestamp)', false, __METHOD__ );
if( $feedFormat || !$wgUseRCPatrol ) {
if( $lastmod && $wgOut->checkLastModified( $lastmod ) ) {
# Client cache fresh and headers sent, nothing more to do.
@@ -278,8 +292,6 @@ class SpecialRecentChanges extends SpecialPage {
$namespace = $opts['namespace'];
$invert = $opts['invert'];
- $join_conds = array();
-
// JOIN on watchlist for users
if( $uid ) {
$tables[] = 'watchlist';
@@ -293,20 +305,23 @@ class SpecialRecentChanges extends SpecialPage {
// Tag stuff.
$fields = array();
// Fields are * in this case, so let the function modify an empty array to keep it happy.
- ChangeTags::modifyDisplayQuery( $tables,
- $fields,
- $conds,
- $join_conds,
- $query_options,
- $opts['tagfilter']
- );
-
- wfRunHooks('SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts ) );
-
- // Is there either one namespace selected or excluded?
- // Tag filtering also has a better index.
- // Also, if this is "all" or main namespace, just use timestamp index.
- if( is_null($namespace) || $invert || $namespace == NS_MAIN || $opts['tagfilter'] ) {
+ ChangeTags::modifyDisplayQuery(
+ $tables, $fields, $conds, $join_conds, $query_options, $opts['tagfilter']
+ );
+
+ if ( !wfRunHooks( 'SpecialRecentChangesQuery', array( &$conds, &$tables, &$join_conds, $opts, &$query_options ) ) )
+ return false;
+
+ // Don't use the new_namespace_time timestamp index if:
+ // (a) "All namespaces" selected
+ // (b) We want all pages NOT in a certain namespaces (inverted)
+ // (c) There is a tag to filter on (use tag index instead)
+ // (d) UNION + sort/limit is not an option for the DBMS
+ if( is_null($namespace)
+ || $invert
+ || $opts['tagfilter'] != ''
+ || !$dbr->unionSupportsOrderAndLimit() )
+ {
$res = $dbr->select( $tables, '*', $conds, __METHOD__,
array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) +
$query_options,
@@ -318,17 +333,18 @@ class SpecialRecentChanges extends SpecialPage {
array( 'rc_new' => 1 ) + $conds,
__METHOD__,
array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit,
- 'USE INDEX' => array('recentchanges' => 'new_name_timestamp') ),
+ 'USE INDEX' => array('recentchanges' => 'rc_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') ),
+ 'USE INDEX' => array('recentchanges' => 'rc_timestamp') ),
$join_conds );
# Join the two fast queries, and sort the result set
- $sql = "($sqlNew) UNION ($sqlOld) ORDER BY rc_timestamp DESC LIMIT $limit";
+ $sql = $dbr->unionQueries(array($sqlNew, $sqlOld), false).' ORDER BY rc_timestamp DESC';
+ $sql = $dbr->limitResult($sql, $limit, false);
$res = $dbr->query( $sql, __METHOD__ );
}
@@ -353,7 +369,7 @@ class SpecialRecentChanges extends SpecialPage {
}
// And now for the content
- $wgOut->setSyndicated( true );
+ $wgOut->setFeedAppendQuery( $this->getFeedQuery() );
if( $wgAllowCategorizedRecentChanges ) {
$this->filterByCategories( $rows, $opts );
@@ -401,6 +417,14 @@ class SpecialRecentChanges extends SpecialPage {
}
/**
+ * Get the query string to append to feed link URLs.
+ * This is overridden by RCL to add the target parameter
+ */
+ public function getFeedQuery() {
+ return false;
+ }
+
+ /**
* Return the text to be displayed above the changes
*
* @param $opts FormOptions
@@ -413,7 +437,7 @@ class SpecialRecentChanges extends SpecialPage {
$defaults = $opts->getAllValues();
$nondefaults = $opts->getChangedValues();
- $opts->consumeValues( array( 'namespace', 'invert' ) );
+ $opts->consumeValues( array( 'namespace', 'invert', 'tagfilter' ) );
$panel = array();
$panel[] = $this->optionsPanel( $defaults, $nondefaults );
@@ -456,6 +480,8 @@ class SpecialRecentChanges extends SpecialPage {
Xml::fieldset( wfMsg( 'recentchanges-legend' ), $panelString, array( 'class' => 'rcoptions' ) )
);
+ $wgOut->addHTML( ChangesList::flagLegend() );
+
$this->setBottomText( $wgOut, $opts );
}
@@ -597,8 +623,12 @@ class SpecialRecentChanges extends SpecialPage {
global $wgUser;
$sk = $wgUser->getSkin();
$params = $override + $options;
- return $sk->link( $this->getTitle(), htmlspecialchars( $title ),
- ( $active ? array( 'style'=>'font-weight: bold;' ) : array() ), $params, array( 'known' ) );
+ if ( $active ) {
+ return $sk->link( $this->getTitle(), '<strong>' . htmlspecialchars( $title ) . '</strong>',
+ array(), $params, array( 'known' ) );
+ } else {
+ return $sk->link( $this->getTitle(), htmlspecialchars( $title ), array() , $params, array( 'known' ) );
+ }
}
/**
@@ -618,7 +648,9 @@ class SpecialRecentChanges extends SpecialPage {
if( $options['from'] ) {
$note .= wfMsgExt( 'rcnotefrom', array( 'parseinline' ),
$wgLang->formatNum( $options['limit'] ),
- $wgLang->timeanddate( $options['from'], true ) ) . '<br />';
+ $wgLang->timeanddate( $options['from'], true ),
+ $wgLang->date( $options['from'], true ),
+ $wgLang->time( $options['from'], true ) ) . '<br />';
}
# Sort data for display and make sure it's unique after we've added user data.
diff --git a/includes/specials/SpecialRecentchangeslinked.php b/includes/specials/SpecialRecentchangeslinked.php
index c58ffff0..3b549843 100644
--- a/includes/specials/SpecialRecentchangeslinked.php
+++ b/includes/specials/SpecialRecentchangeslinked.php
@@ -5,6 +5,7 @@
* @ingroup SpecialPage
*/
class SpecialRecentchangeslinked extends SpecialRecentchanges {
+ var $rclTargetTitle;
function __construct(){
SpecialPage::SpecialPage( 'Recentchangeslinked' );
@@ -26,7 +27,6 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
public function feedSetup() {
global $wgRequest;
$opts = parent::feedSetup();
- # Feed is cached on limit,hideminor,target; other params would randomly not work
$opts['target'] = $wgRequest->getVal( 'target' );
return $opts;
}
@@ -34,8 +34,8 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
public function getFeedObject( $feedFormat ){
$feed = new ChangesFeed( $feedFormat, false );
$feedObj = $feed->getFeedObject(
- wfMsgForContent( 'recentchangeslinked-title', $this->mTargetTitle->getPrefixedText() ),
- wfMsgForContent( 'recentchangeslinked' )
+ wfMsgForContent( 'recentchangeslinked-title', $this->getTargetTitle()->getPrefixedText() ),
+ wfMsgForContent( 'recentchangeslinked-feed' )
);
return array( $feed, $feedObj );
}
@@ -52,10 +52,9 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
}
$title = Title::newFromURL( $target );
if( !$title || $title->getInterwiki() != '' ){
- $wgOut->wrapWikiMsg( '<div class="errorbox">$1</div><br clear="both" />', 'allpagesbadtitle' );
+ $wgOut->wrapWikiMsg( "<div class=\"errorbox\">\n$1</div><br style=\"clear: both\" />", 'allpagesbadtitle' );
return false;
}
- $this->mTargetTitle = $title;
$wgOut->setPageTitle( wfMsg( 'recentchangeslinked-title', $title->getPrefixedText() ) );
@@ -84,6 +83,11 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
$select[] = 'wl_user';
$join_conds['watchlist'] = array( 'LEFT JOIN', "wl_user={$uid} AND wl_title=rc_title AND wl_namespace=rc_namespace" );
}
+ if ( $wgUser->isAllowed( 'rollback' ) ) {
+ $tables[] = 'page';
+ $join_conds['page'] = array('LEFT JOIN', 'rc_cur_id=page_id');
+ $select[] = 'page_latest';
+ }
ChangeTags::modifyDisplayQuery( $tables, $select, $conds, $join_conds,
$query_options, $opts['tagfilter'] );
@@ -139,25 +143,37 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
}
}
- $subsql[] = $dbr->selectSQLText(
+ if( $dbr->unionSupportsOrderAndLimit())
+ $order = array( 'ORDER BY' => 'rc_timestamp DESC' );
+ else
+ $order = array();
+
+
+ $query = $dbr->selectSQLText(
array_merge( $tables, array( $link_table ) ),
$select,
$conds + $subconds,
__METHOD__,
- array( 'ORDER BY' => 'rc_timestamp DESC', 'LIMIT' => $limit ) + $query_options,
+ $order + $query_options,
$join_conds + array( $link_table => array( 'INNER JOIN', $subjoin ) )
);
+
+ if( $dbr->unionSupportsOrderAndLimit())
+ $query = $dbr->limitResult( $query, $limit );
+
+ $subsql[] = $query;
}
if( count($subsql) == 0 )
return false; // should never happen
- if( count($subsql) == 1 )
+ if( count($subsql) == 1 && $dbr->unionSupportsOrderAndLimit() )
$sql = $subsql[0];
else {
// need to resort and relimit after union
- $sql = "(" . implode( ") UNION (", $subsql ) . ") ORDER BY rc_timestamp DESC LIMIT {$limit}";
+ $sql = $dbr->unionQueries($subsql, false).' ORDER BY rc_timestamp DESC';
+ $sql = $dbr->limitResult($sql, $limit, false);
}
-
+
$res = $dbr->query( $sql, __METHOD__ );
if( $res->numRows() == 0 )
@@ -167,10 +183,10 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
}
function getExtraOptions( $opts ){
- $opts->consumeValues( array( 'showlinkedto', 'target' ) );
+ $opts->consumeValues( array( 'showlinkedto', 'target', 'tagfilter' ) );
$extraOpts = array();
$extraOpts['namespace'] = $this->namespaceFilterForm( $opts );
- $extraOpts['target'] = array( wfMsg( 'recentchangeslinked-page' ),
+ $extraOpts['target'] = array( wfMsgHtml( 'recentchangeslinked-page' ),
Xml::input( 'target', 40, str_replace('_',' ',$opts['target']) ) .
Xml::check( 'showlinkedto', $opts['showlinkedto'], array('id' => 'showlinkedto') ) . ' ' .
Xml::label( wfMsg("recentchangeslinked-to"), 'showlinkedto' ) );
@@ -180,19 +196,37 @@ class SpecialRecentchangeslinked extends SpecialRecentchanges {
return $extraOpts;
}
+ function getTargetTitle() {
+ if ( $this->rclTargetTitle === null ) {
+ $opts = $this->getOptions();
+ if ( isset( $opts['target'] ) && $opts['target'] !== '' ) {
+ $this->rclTargetTitle = Title::newFromText( $opts['target'] );
+ } else {
+ $this->rclTargetTitle = false;
+ }
+ }
+ return $this->rclTargetTitle;
+ }
+
function setTopText( OutputPage $out, FormOptions $opts ) {
global $wgUser;
$skin = $wgUser->getSkin();
- if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) )
- $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $this->mTargetTitle,
- $this->mTargetTitle->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) );
+ $target = $this->getTargetTitle();
+ if( $target )
+ $out->setSubtitle( wfMsg( 'recentchangeslinked-backlink', $skin->link( $target,
+ $target->getPrefixedText(), array(), array( 'redirect' => 'no' ) ) ) );
}
- function setBottomText( OutputPage $out, FormOptions $opts ){
- if( isset( $this->mTargetTitle ) && is_object( $this->mTargetTitle ) ){
- global $wgUser;
- $out->setFeedAppendQuery( "target=" . urlencode( $this->mTargetTitle->getPrefixedDBkey() ) );
+ public function getFeedQuery() {
+ $target = $this->getTargetTitle();
+ if( $target ) {
+ return "target=" . urlencode( $target->getPrefixedDBkey() );
+ } else {
+ return false;
}
+ }
+
+ function setBottomText( OutputPage $out, FormOptions $opts ) {
if( isset( $this->mResultEmpty ) && $this->mResultEmpty ){
$out->addWikiMsg( 'recentchangeslinked-noresult' );
}
diff --git a/includes/specials/SpecialRemoveRestrictions.php b/includes/specials/SpecialRemoveRestrictions.php
index ded6cbe3..a3428a5a 100644
--- a/includes/specials/SpecialRemoveRestrictions.php
+++ b/includes/specials/SpecialRemoveRestrictions.php
@@ -1,9 +1,9 @@
<?php
function wfSpecialRemoveRestrictions() {
- global $wgOut, $wgRequest, $wgUser, $wgLang, $wgTitle;
+ global $wgOut, $wgRequest, $wgUser, $wgLang;
$sk = $wgUser->getSkin();
-
+ $title = SpecialPage::getTitleFor( 'RemoveRestrictions' );
$id = $wgRequest->getVal( 'id' );
if( !is_numeric( $id ) ) {
$wgOut->addWikiMsg( 'removerestrictions-noid' );
@@ -36,17 +36,17 @@ function wfSpecialRemoveRestrictions() {
if( $result )
$wgOut->addHTML( '<strong class="success">' . wfMsgExt( 'removerestrictions-success',
'parseinline', $r->getSubjectText() ) . '</strong>' );
- $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl( array( 'id' => $id ) ),
+ $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $title->getLocalUrl( array( 'id' => $id ) ),
'method' => 'post' ) ) );
$wgOut->addHTML( Xml::buildForm( $form, 'removerestrictions-submit' ) );
$wgOut->addHTML( Xml::hidden( 'id', $r->getId() ) );
- $wgOut->addHTML( Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ) );
+ $wgOut->addHTML( Xml::hidden( 'title', $title->getPrefixedDbKey() ) );
$wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) );
$wgOut->addHTML( "</form></fieldset>" );
}
function wfSpecialRemoveRestrictionsProcess( $r ) {
- global $wgUser, $wgRequest;
+ global $wgRequest;
$reason = $wgRequest->getVal( 'reason' );
$result = $r->delete();
$log = new LogPage( 'restrict' );
diff --git a/includes/specials/SpecialResetpass.php b/includes/specials/SpecialResetpass.php
index 059f8dbd..967d2119 100644
--- a/includes/specials/SpecialResetpass.php
+++ b/includes/specials/SpecialResetpass.php
@@ -37,6 +37,11 @@ class SpecialResetpass extends SpecialPage {
return;
}
+ if( $wgRequest->wasPosted() && $wgRequest->getBool( 'wpCancel' ) ) {
+ $this->doReturnTo();
+ return;
+ }
+
if( $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal('token') ) ) {
try {
$this->attemptReset( $this->mNewpass, $this->mRetype );
@@ -54,17 +59,22 @@ class SpecialResetpass extends SpecialPage {
$login = new LoginForm( new FauxRequest( $data, true ) );
$login->execute();
}
- $titleObj = Title::newFromText( $wgRequest->getVal( 'returnto' ) );
- if ( !$titleObj instanceof Title ) {
- $titleObj = Title::newMainPage();
- }
- $wgOut->redirect( $titleObj->getFullURL() );
+ $this->doReturnTo();
} catch( PasswordError $e ) {
$this->error( $e->getMessage() );
}
}
$this->showForm();
}
+
+ function doReturnTo() {
+ global $wgRequest, $wgOut;
+ $titleObj = Title::newFromText( $wgRequest->getVal( 'returnto' ) );
+ if ( !$titleObj instanceof Title ) {
+ $titleObj = Title::newMainPage();
+ }
+ $wgOut->redirect( $titleObj->getFullURL() );
+ }
function error( $msg ) {
global $wgOut;
@@ -102,52 +112,60 @@ class SpecialResetpass extends SpecialPage {
array(
'method' => 'post',
'action' => $self->getLocalUrl(),
- 'id' => 'mw-resetpass-form' ) ) .
- Xml::hidden( 'token', $wgUser->editToken() ) .
- Xml::hidden( 'wpName', $this->mUserName ) .
- Xml::hidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) .
- wfMsgExt( 'resetpass_text', array( 'parse' ) ) .
- Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) .
+ 'id' => 'mw-resetpass-form' ) ) . "\n" .
+ Xml::hidden( 'token', $wgUser->editToken() ) . "\n" .
+ Xml::hidden( 'wpName', $this->mUserName ) . "\n" .
+ Xml::hidden( 'returnto', $wgRequest->getVal( 'returnto' ) ) . "\n" .
+ wfMsgExt( 'resetpass_text', array( 'parse' ) ) . "\n" .
+ Xml::openElement( 'table', array( 'id' => 'mw-resetpass-table' ) ) . "\n" .
$this->pretty( array(
array( 'wpName', 'username', 'text', $this->mUserName ),
array( 'wpPassword', $oldpassMsg, 'password', $this->mOldpass ),
- array( 'wpNewPassword', 'newpassword', 'password', '' ),
- array( 'wpRetype', 'retypenew', 'password', '' ),
- ) ) .
+ array( 'wpNewPassword', 'newpassword', 'password', null ),
+ array( 'wpRetype', 'retypenew', 'password', null ),
+ ) ) . "\n" .
$rememberMe .
- '<tr>' .
- '<td></td>' .
+ "<tr>\n" .
+ "<td></td>\n" .
'<td class="mw-input">' .
Xml::submitButton( wfMsg( $submitMsg ) ) .
- '</td>' .
- '</tr>' .
+ Xml::submitButton( wfMsg( 'resetpass-submit-cancel' ), array( 'name' => 'wpCancel' ) ) .
+ "</td>\n" .
+ "</tr>\n" .
Xml::closeElement( 'table' ) .
Xml::closeElement( 'form' ) .
- Xml::closeElement( 'fieldset' )
+ Xml::closeElement( 'fieldset' ) . "\n"
);
}
function pretty( $fields ) {
$out = '';
- foreach( $fields as $list ) {
+ foreach ( $fields as $list ) {
list( $name, $label, $type, $value ) = $list;
if( $type == 'text' ) {
$field = htmlspecialchars( $value );
} else {
- $field = Xml::input( $name, 20, $value,
- array( 'id' => $name, 'type' => $type ) );
+ $attribs = array( 'id' => $name );
+ if ( $name == 'wpNewPassword' || $name == 'wpRetype' ) {
+ $attribs = array_merge( $attribs,
+ User::passwordChangeInputAttribs() );
+ }
+ if ( $name == 'wpPassword' ) {
+ $attribs[] = 'autofocus';
+ }
+ $field = Html::input( $name, $value, $type, $attribs );
}
- $out .= '<tr>';
- $out .= "<td class='mw-label'>";
+ $out .= "<tr>\n";
+ $out .= "\t<td class='mw-label'>";
if ( $type != 'text' )
$out .= Xml::label( wfMsg( $label ), $name );
else
- $out .= wfMsg( $label );
- $out .= '</td>';
- $out .= "<td class='mw-input'>";
+ $out .= wfMsgHtml( $label );
+ $out .= "</td>\n";
+ $out .= "\t<td class='mw-input'>";
$out .= $field;
- $out .= '</td>';
- $out .= '</tr>';
+ $out .= "</td>\n";
+ $out .= "</tr>";
}
return $out;
}
diff --git a/includes/specials/SpecialRestrictUser.php b/includes/specials/SpecialRestrictUser.php
deleted file mode 100644
index b946cde8..00000000
--- a/includes/specials/SpecialRestrictUser.php
+++ /dev/null
@@ -1,190 +0,0 @@
-<?php
-
-function wfSpecialRestrictUser( $par = null ) {
- global $wgOut, $wgRequest;
- $user = $userOrig = null;
- if( $par ) {
- $userOrig = $par;
- } elseif( $wgRequest->getVal( 'user' ) ) {
- $userOrig = $wgRequest->getVal( 'user' );
- } else {
- $wgOut->addHTML( RestrictUserForm::selectUserForm() );
- return;
- }
- $isIP = User::isIP( $userOrig );
- $user = $isIP ? $userOrig : User::getCanonicalName( $userOrig );
- $uid = User::idFromName( $user );
- if( !$uid && !$isIP ) {
- $err = '<strong class="error">' . wfMsgHtml( 'restrictuser-notfound' ) . '</strong>';
- $wgOut->addHTML( RestrictUserForm::selectUserForm( $userOrig, $err ) );
- return;
- }
- $wgOut->addHTML( RestrictUserForm::selectUserForm( $user ) );
-
- UserRestriction::purgeExpired();
- $old = UserRestriction::fetchForUser( $user, true );
-
- RestrictUserForm::pageRestrictionForm( $uid, $user, $old );
- RestrictUserForm::namespaceRestrictionForm( $uid, $user, $old );
-
- // Renew it after possible changes in previous two functions
- $old = UserRestriction::fetchForUser( $user, true );
- if( $old ) {
- $wgOut->addHTML( RestrictUserForm::existingRestrictions( $old ) );
- }
-}
-
-class RestrictUserForm {
- public static function selectUserForm( $val = null, $error = null ) {
- global $wgScript, $wgTitle;
- $action = htmlspecialchars( $wgScript );
- $s = Xml::fieldset( wfMsg( 'restrictuser-userselect' ) ) . "<form action=\"{$action}\">";
- if( $error )
- $s .= '<p>' . $error . '</p>';
- $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() );
- $form = array( 'restrictuser-user' => Xml::input( 'user', false, $val ) );
- $s .= Xml::buildForm( $form, 'restrictuser-go' );
- $s .= "</form></fieldset>";
- return $s;
- }
-
- public static function existingRestrictions( $restrictions ) {
- //TODO: autoload?
- require_once( dirname( __FILE__ ) . '/SpecialListUserRestrictions.php' );
- $s = Xml::fieldset( wfMsg( 'restrictuser-existing' ) ) . '<ul>';
- foreach( $restrictions as $r )
- $s .= UserRestrictionsPager::formatRestriction( $r );
- $s .= "</ul></fieldset>";
- return $s;
- }
-
- public static function pageRestrictionForm( $uid, $user, $oldRestrictions ) {
- global $wgOut, $wgTitle, $wgRequest, $wgUser;
- $error = '';
- $success = false;
- if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::PAGE &&
- $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) {
-
- $title = Title::newFromText( $wgRequest->getVal( 'page' ) );
- if( !$title ) {
- $error = array( 'restrictuser-badtitle', $wgRequest->getVal( 'page' ) );
- } elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false ) {
- $error = array( 'restrictuser-badexpiry', $wgRequest->getVal( 'expiry' ) );
- } else {
- foreach( $oldRestrictions as $r ) {
- if( $r->isPage() && $r->getPage()->equals( $title ) )
- $error = array( 'restrictuser-duptitle' );
- }
- }
- if( !$error ) {
- self::doPageRestriction( $uid, $user );
- $success = array('restrictuser-success', $user);
- }
- }
- $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::PAGE;
- $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-page' ) ) );
-
- self::printSuccessError( $success, $error );
-
- $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(),
- 'method' => 'post' ) ) );
- $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::PAGE ) );
- $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) );
- $wgOut->addHTML( Xml::hidden( 'user', $user ) );
- $form = array();
- $form['restrictuser-title'] = Xml::input( 'page', false,
- $useRequestValues ? $wgRequest->getVal( 'page' ) : false );
- $form['restrictuser-expiry'] = Xml::input( 'expiry', false,
- $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false );
- $form['restrictuser-reason'] = Xml::input( 'reason', false,
- $useRequestValues ? $wgRequest->getVal( 'reason' ) : false );
- $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) );
- $wgOut->addHTML( "</form></fieldset>" );
- }
-
- public static function printSuccessError( $success, $error ) {
- global $wgOut;
- if ( $error )
- $wgOut->wrapWikiMsg( '<strong class="error">$1</strong>', $error );
- if ( $success )
- $wgOut->wrapWikiMsg( '<strong class="success">$1</strong>', $success );
- }
-
- public static function doPageRestriction( $uid, $user ) {
- global $wgUser, $wgRequest;
- $r = new UserRestriction();
- $r->setType( UserRestriction::PAGE );
- $r->setPage( Title::newFromText( $wgRequest->getVal( 'page' ) ) );
- $r->setSubjectId( $uid );
- $r->setSubjectText( $user );
- $r->setBlockerId( $wgUser->getId() );
- $r->setBlockerText( $wgUser->getName() );
- $r->setReason( $wgRequest->getVal( 'reason' ) );
- $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) );
- $r->setTimestamp( wfTimestampNow( TS_MW ) );
- $r->commit();
- $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity();
- $l = new LogPage( 'restrict' );
- $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(),
- array( $r->getType(), $r->getPage()->getFullText(), $logExpiry) );
- }
-
- public static function namespaceRestrictionForm( $uid, $user, $oldRestrictions ) {
- global $wgOut, $wgTitle, $wgRequest, $wgUser, $wgContLang;
- $error = '';
- $success = false;
- if( $wgRequest->wasPosted() && $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE &&
- $wgUser->matchEditToken( $wgRequest->getVal( 'edittoken' ) ) ) {
- $ns = $wgRequest->getVal( 'namespace' );
- if( $wgContLang->getNsText( $ns ) === false )
- $error = wfMsgExt( 'restrictuser-badnamespace', 'parseinline' );
- elseif( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) === false )
- $error = wfMsgExt( 'restrictuser-badexpiry', 'parseinline', $wgRequest->getVal( 'expiry' ) );
- else
- foreach( $oldRestrictions as $r )
- if( $r->isNamespace() && $r->getNamespace() == $ns )
- $error = wfMsgExt( 'restrictuser-dupnamespace', 'parse' );
- if( !$error ) {
- self::doNamespaceRestriction( $uid, $user );
- $success = array('restrictuser-success', $user);
- }
- }
- $useRequestValues = $wgRequest->getVal( 'type' ) == UserRestriction::NAMESPACE;
- $wgOut->addHTML( Xml::fieldset( wfMsg( 'restrictuser-legend-namespace' ) ) );
-
- self::printSuccessError( $success, $error );
-
- $wgOut->addHTML( Xml::openElement( 'form', array( 'action' => $wgTitle->getLocalUrl(),
- 'method' => 'post' ) ) );
- $wgOut->addHTML( Xml::hidden( 'type', UserRestriction::NAMESPACE ) );
- $wgOut->addHTML( Xml::hidden( 'edittoken', $wgUser->editToken() ) );
- $wgOut->addHTML( Xml::hidden( 'user', $user ) );
- $form = array();
- $form['restrictuser-namespace'] = Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ) );
- $form['restrictuser-expiry'] = Xml::input( 'expiry', false,
- $useRequestValues ? $wgRequest->getVal( 'expiry' ) : false );
- $form['restrictuser-reason'] = Xml::input( 'reason', false,
- $useRequestValues ? $wgRequest->getVal( 'reason' ) : false );
- $wgOut->addHTML( Xml::buildForm( $form, 'restrictuser-submit' ) );
- $wgOut->addHTML( "</form></fieldset>" );
- }
-
- public static function doNamespaceRestriction( $uid, $user ) {
- global $wgUser, $wgRequest;
- $r = new UserRestriction();
- $r->setType( UserRestriction::NAMESPACE );
- $r->setNamespace( $wgRequest->getVal( 'namespace' ) );
- $r->setSubjectId( $uid );
- $r->setSubjectText( $user );
- $r->setBlockerId( $wgUser->getId() );
- $r->setBlockerText( $wgUser->getName() );
- $r->setReason( $wgRequest->getVal( 'reason' ) );
- $r->setExpiry( UserRestriction::convertExpiry( $wgRequest->getVal( 'expiry' ) ) );
- $r->setTimestamp( wfTimestampNow( TS_MW ) );
- $r->commit();
- $logExpiry = $wgRequest->getVal( 'expiry' ) ? $wgRequest->getVal( 'expiry' ) : Block::infinity();
- $l = new LogPage( 'restrict' );
- $l->addEntry( 'restrict', Title::makeTitle( NS_USER, $user ), $r->getReason(),
- array( $r->getType(), $r->getNamespace(), $logExpiry ) );
- }
-}
diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php
index 7fdb3cc4..b2db869c 100644
--- a/includes/specials/SpecialRevisiondelete.php
+++ b/includes/specials/SpecialRevisiondelete.php
@@ -8,164 +8,287 @@
*/
class SpecialRevisionDelete extends UnlistedSpecialPage {
+ /** Skin object */
+ var $skin;
+
+ /** True if the submit button was clicked, and the form was posted */
+ var $submitClicked;
+
+ /** Target ID list */
+ var $ids;
+
+ /** Archive name, for reviewing deleted files */
+ var $archiveName;
+
+ /** Edit token for securing image views against XSS */
+ var $token;
+
+ /** Title object for target parameter */
+ var $targetObj;
+
+ /** Deletion type, may be revision, archive, oldimage, filearchive, logging. */
+ var $typeName;
+
+ /** Array of checkbox specs (message, name, deletion bits) */
+ var $checks;
+
+ /** Information about the current type */
+ var $typeInfo;
+
+ /** The RevDel_List object, storing the list of items to be deleted/undeleted */
+ var $list;
+
+ /**
+ * Assorted information about each type, needed by the special page.
+ * TODO Move some of this to the list class
+ */
+ static $allowedTypes = array(
+ 'revision' => array(
+ 'check-label' => 'revdelete-hide-text',
+ 'deletion-bits' => Revision::DELETED_TEXT,
+ 'success' => 'revdelete-success',
+ 'failure' => 'revdelete-failure',
+ 'list-class' => 'RevDel_RevisionList',
+ ),
+ 'archive' => array(
+ 'check-label' => 'revdelete-hide-text',
+ 'deletion-bits' => Revision::DELETED_TEXT,
+ 'success' => 'revdelete-success',
+ 'failure' => 'revdelete-failure',
+ 'list-class' => 'RevDel_ArchiveList',
+ ),
+ 'oldimage'=> array(
+ 'check-label' => 'revdelete-hide-image',
+ 'deletion-bits' => File::DELETED_FILE,
+ 'success' => 'revdelete-success',
+ 'failure' => 'revdelete-failure',
+ 'list-class' => 'RevDel_FileList',
+ ),
+ 'filearchive' => array(
+ 'check-label' => 'revdelete-hide-image',
+ 'deletion-bits' => File::DELETED_FILE,
+ 'success' => 'revdelete-success',
+ 'failure' => 'revdelete-failure',
+ 'list-class' => 'RevDel_ArchivedFileList',
+ ),
+ 'logging' => array(
+ 'check-label' => 'revdelete-hide-name',
+ 'deletion-bits' => LogPage::DELETED_ACTION,
+ 'success' => 'logdelete-success',
+ 'failure' => 'logdelete-failure',
+ 'list-class' => 'RevDel_LogList',
+ ),
+ );
+
+ /** Type map to support old log entries */
+ static $deprecatedTypeMap = array(
+ 'oldid' => 'revision',
+ 'artimestamp' => 'archive',
+ 'oldimage' => 'oldimage',
+ 'fileid' => 'filearchive',
+ 'logid' => 'logging',
+ );
public function __construct() {
- parent::__construct( 'Revisiondelete', 'deleterevision' );
- $this->includable( false );
+ parent::__construct( 'Revisiondelete', 'deletedhistory' );
}
public function execute( $par ) {
global $wgOut, $wgUser, $wgRequest;
- if( wfReadOnly() ) {
- $wgOut->readOnlyPage();
+ if( !$wgUser->isAllowed( 'deletedhistory' ) ) {
+ $wgOut->permissionRequired( 'deletedhistory' );
return;
- }
- if( !$wgUser->isAllowed( 'deleterevision' ) ) {
- $wgOut->permissionRequired( 'deleterevision' );
+ } else if( wfReadOnly() ) {
+ $wgOut->readOnlyPage();
return;
}
- $this->skin =& $wgUser->getSkin();
- # Set title and such
+ $this->mIsAllowed = $wgUser->isAllowed('deleterevision'); // for changes
+ $this->skin = $wgUser->getSkin();
$this->setHeaders();
$this->outputHeader();
- $this->wasPosted = $wgRequest->wasPosted();
- # Handle our many different possible input types
- $this->target = $wgRequest->getText( 'target' );
- $this->oldids = $wgRequest->getArray( 'oldid' );
- $this->artimestamps = $wgRequest->getArray( 'artimestamp' );
- $this->logids = $wgRequest->getArray( 'logid' );
- $this->oldimgs = $wgRequest->getArray( 'oldimage' );
- $this->fileids = $wgRequest->getArray( 'fileid' );
+ $this->submitClicked = $wgRequest->wasPosted() && $wgRequest->getBool( 'wpSubmit' );
+ # Handle our many different possible input types.
+ $ids = $wgRequest->getVal( 'ids' );
+ if ( !is_null( $ids ) ) {
+ # Allow CSV, for backwards compatibility, or a single ID for show/hide links
+ $this->ids = explode( ',', $ids );
+ } else {
+ # Array input
+ $this->ids = array_keys( $wgRequest->getArray('ids',array()) );
+ }
+ // $this->ids = array_map( 'intval', $this->ids );
+ $this->ids = array_unique( array_filter( $this->ids ) );
+
+ if ( $wgRequest->getVal( 'action' ) == 'historysubmit' ) {
+ # For show/hide form submission from history page
+ $this->targetObj = $GLOBALS['wgTitle'];
+ $this->typeName = 'revision';
+ } else {
+ $this->typeName = $wgRequest->getVal( 'type' );
+ $this->targetObj = Title::newFromText( $wgRequest->getText( 'target' ) );
+ }
+
# For reviewing deleted files...
- $this->file = $wgRequest->getVal( 'file' );
- # Only one target set at a time please!
- $i = (bool)$this->file + (bool)$this->oldids + (bool)$this->logids
- + (bool)$this->artimestamps + (bool)$this->fileids + (bool)$this->oldimgs;
- # No targets?
- if( $i == 0 ) {
- $wgOut->showErrorPage( 'notargettitle', 'notargettext' );
+ $this->archiveName = $wgRequest->getVal( 'file' );
+ $this->token = $wgRequest->getVal( 'token' );
+ if ( $this->archiveName && $this->targetObj ) {
+ $this->tryShowFile( $this->archiveName );
return;
}
- # Too many targets?
- if( $i !== 1 ) {
- $wgOut->showErrorPage( 'revdelete-toomanytargets-title', 'revdelete-toomanytargets-text' );
+
+ if ( isset( self::$deprecatedTypeMap[$this->typeName] ) ) {
+ $this->typeName = self::$deprecatedTypeMap[$this->typeName];
+ }
+
+ # No targets?
+ if( !isset( self::$allowedTypes[$this->typeName] ) || count( $this->ids ) == 0 ) {
+ $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
return;
}
- $this->page = Title::newFromUrl( $this->target );
+ $this->typeInfo = self::$allowedTypes[$this->typeName];
+
# If we have revisions, get the title from the first one
# since they should all be from the same page. This allows
# for more flexibility with page moves...
- if( count($this->oldids) > 0 ) {
- $rev = Revision::newFromId( $this->oldids[0] );
- $this->page = $rev ? $rev->getTitle() : $this->page;
+ if( $this->typeName == 'revision' ) {
+ $rev = Revision::newFromId( $this->ids[0] );
+ $this->targetObj = $rev ? $rev->getTitle() : $this->targetObj;
}
+
+ $this->otherReason = $wgRequest->getVal( 'wpReason' );
# We need a target page!
- if( is_null($this->page) ) {
+ if( is_null($this->targetObj) ) {
$wgOut->addWikiMsg( 'undelete-header' );
return;
}
- # Logs must have a type given
- if( $this->logids && !strpos($this->page->getDBKey(),'/') ) {
- $wgOut->showErrorPage( 'revdelete-nologtype-title', 'revdelete-nologtype-text' );
- return;
- }
- # For reviewing deleted files...show it now if allowed
- if( $this->file ) {
- $oimage = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $this->page, $this->file );
- $oimage->load();
- // Check if user is allowed to see this file
- if( !$oimage->userCan(File::DELETED_FILE) ) {
- $wgOut->permissionRequired( 'suppressrevision' );
- } else {
- $this->showFile( $this->file );
- }
- return;
- }
# Give a link to the logs/hist for this page
- if( !is_null($this->page) && $this->page->getNamespace() > -1 ) {
- $links = array();
+ $this->showConvenienceLinks();
- $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->getPrefixedDBkey() ) ) );
- }
- # Logs themselves don't have histories or archived revisions
- $wgOut->setSubtitle( '<p>'.implode($links,' / ').'</p>' );
- }
- # Lock the operation and the form context
- $this->secureOperation();
- # Either submit or create our form
- if( $this->wasPosted ) {
- $this->submit( $wgRequest );
- } else if( $this->deleteKey == 'oldid' || $this->deleteKey == 'artimestamp' ) {
- $this->showRevs();
- } else if( $this->deleteKey == 'fileid' || $this->deleteKey == 'oldimage' ) {
- $this->showImages();
- } else if( $this->deleteKey == 'logid' ) {
- $this->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', $this->page->getPrefixedText() );
- if( $wgUser->isAllowed( 'suppressionlog' ) ){
- $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
- LogEventsList::showLogExtract( $wgOut, 'suppress', $this->page->getPrefixedText() );
- }
- }
-
- private function secureOperation() {
- global $wgUser;
- $this->deleteKey = '';
- // At this point, we should only have one of these
- if( $this->oldids ) {
- $this->revisions = $this->oldids;
- $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
- $this->deleteKey = 'oldid';
- } else if( $this->artimestamps ) {
- $this->archrevs = $this->artimestamps;
- $hide_content_name = array( 'revdelete-hide-text', 'wpHideText', Revision::DELETED_TEXT );
- $this->deleteKey = 'artimestamp';
- } else if( $this->oldimgs ) {
- $this->ofiles = $this->oldimgs;
- $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
- $this->deleteKey = 'oldimage';
- } else if( $this->fileids ) {
- $this->afiles = $this->fileids;
- $hide_content_name = array( 'revdelete-hide-image', 'wpHideImage', File::DELETED_FILE );
- $this->deleteKey = 'fileid';
- } else if( $this->logids ) {
- $this->events = $this->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
+ # Initialise checkboxes
$this->checks = array(
- $hide_content_name,
+ array( $this->typeInfo['check-label'], 'wpHidePrimary', $this->typeInfo['deletion-bits'] ),
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 );
+ $this->checks[] = array( 'revdelete-hide-restricted',
+ 'wpHideRestricted', Revision::DELETED_RESTRICTED );
+ }
+
+ # Either submit or create our form
+ if( $this->mIsAllowed && $this->submitClicked ) {
+ $this->submit( $wgRequest );
+ } else {
+ $this->showForm();
+ }
+
+ $qc = $this->getLogQueryCond();
+ # Show relevant lines from the deletion log
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'delete' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'delete',
+ $this->targetObj->getPrefixedText(), '', array( 'lim' => 25, 'conds' => $qc ) );
+ # Show relevant lines from the suppression log
+ if( $wgUser->isAllowed( 'suppressionlog' ) ) {
+ $wgOut->addHTML( "<h2>" . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "</h2>\n" );
+ LogEventsList::showLogExtract( $wgOut, 'suppress',
+ $this->targetObj->getPrefixedText(), '', array( 'lim' => 25, 'conds' => $qc ) );
}
}
/**
+ * Show some useful links in the subtitle
+ */
+ protected function showConvenienceLinks() {
+ global $wgOut, $wgUser, $wgLang;
+ # Give a link to the logs/hist for this page
+ if( $this->targetObj ) {
+ $links = array();
+ $links[] = $this->skin->linkKnown(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'viewpagelogs' ),
+ array(),
+ array( 'page' => $this->targetObj->getPrefixedText() )
+ );
+ if ( $this->targetObj->getNamespace() != NS_SPECIAL ) {
+ # Give a link to the page history
+ $links[] = $this->skin->linkKnown(
+ $this->targetObj,
+ wfMsgHtml( 'pagehist' ),
+ array(),
+ array( 'action' => 'history' )
+ );
+ # Link to deleted edits
+ if( $wgUser->isAllowed('undelete') ) {
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $links[] = $this->skin->linkKnown(
+ $undelete,
+ wfMsgHtml( 'deletedhist' ),
+ array(),
+ array( 'target' => $this->targetObj->getPrefixedDBkey() )
+ );
+ }
+ }
+ # Logs themselves don't have histories or archived revisions
+ $wgOut->setSubtitle( '<p>' . $wgLang->pipeList( $links ) . '</p>' );
+ }
+ }
+
+ /**
+ * Get the condition used for fetching log snippets
+ */
+ protected function getLogQueryCond() {
+ $conds = array();
+ // Revision delete logs for these item
+ $conds['log_type'] = array('delete','suppress');
+ $conds['log_action'] = $this->getList()->getLogAction();
+ $conds['ls_field'] = RevisionDeleter::getRelationType( $this->typeName );
+ $conds['ls_value'] = $this->ids;
+ return $conds;
+ }
+
+ /**
* Show a deleted file version requested by the visitor.
+ * TODO Mostly copied from Special:Undelete. Refactor.
*/
- private function showFile( $key ) {
- global $wgOut, $wgRequest;
+ protected function tryShowFile( $archiveName ) {
+ global $wgOut, $wgRequest, $wgUser, $wgLang;
+
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $oimage = $repo->newFromArchiveName( $this->targetObj, $archiveName );
+ $oimage->load();
+ // Check if user is allowed to see this file
+ if ( !$oimage->exists() ) {
+ $wgOut->addWikiMsg( 'revdelete-no-file' );
+ return;
+ }
+ if( !$oimage->userCan(File::DELETED_FILE) ) {
+ if( $oimage->isDeleted( File::DELETED_RESTRICTED ) ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ } else {
+ $wgOut->permissionRequired( 'deletedtext' );
+ }
+ return;
+ }
+ if ( !$wgUser->matchEditToken( $this->token, $archiveName ) ) {
+ $wgOut->addWikiMsg( 'revdelete-show-file-confirm',
+ $this->targetObj->getText(),
+ $wgLang->date( $oimage->getTimestamp() ),
+ $wgLang->time( $oimage->getTimestamp() ) );
+ $wgOut->addHTML(
+ Xml::openElement( 'form', array(
+ 'method' => 'POST',
+ 'action' => $this->getTitle()->getLocalUrl(
+ 'target=' . urlencode( $oimage->getName() ) .
+ '&file=' . urlencode( $archiveName ) .
+ '&token=' . urlencode( $wgUser->editToken( $archiveName ) ) )
+ )
+ ) .
+ Xml::submitButton( wfMsg( 'revdelete-show-file-submit' ) ) .
+ '</form>'
+ );
+ return;
+ }
$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
@@ -174,103 +297,61 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
$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 );
+ # Stream the file to the client
+ global $IP;
+ require_once( "$IP/includes/StreamFile.php" );
+ $key = $oimage->getStorageKey();
+ $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
+ wfStreamFile( $path );
}
/**
- * This lets a user set restrictions for live and archived revisions
+ * Get the list object for this request
*/
- private function showRevs() {
- global $wgOut, $wgUser;
+ protected function getList() {
+ if ( is_null( $this->list ) ) {
+ $class = $this->typeInfo['list-class'];
+ $this->list = new $class( $this, $this->targetObj, $this->ids );
+ }
+ return $this->list;
+ }
+
+ /**
+ * Show a list of items that we will operate on, and show a form with checkboxes
+ * which will allow the user to choose new visibility settings.
+ */
+ protected function showForm() {
+ global $wgOut, $wgUser, $wgLang;
$UserAllowed = true;
- $count = ($this->deleteKey=='oldid') ?
- count($this->revisions) : count($this->archrevs);
- $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count );
+ if ( $this->typeName == 'logging' ) {
+ $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->ids) ) );
+ } else {
+ $wgOut->addWikiMsg( 'revdelete-selected',
+ $this->targetObj->getPrefixedText(), count( $this->ids ) );
+ }
- $bitfields = 0;
$wgOut->addHTML( "<ul>" );
$where = $revObjs = array();
- $dbr = wfGetDB( DB_MASTER );
- $revisions = 0;
+ $numRevisions = 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);
- }
- $result = $dbr->select( array('revision','page'), '*',
- array(
- 'rev_page' => $this->page->getArticleID(),
- 'rev_id' => $where,
- '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( !$this->wasPosted ) {
- $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->timestamp( $timestamp );
- }
- $result = $dbr->select( 'archive', '*',
- array(
- 'ar_namespace' => $this->page->getNamespace(),
- 'ar_title' => $this->page->getDBKey(),
- 'ar_timestamp' => $where ),
- __METHOD__ );
- while( $row = $dbr->fetchObject( $result ) ) {
- $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
- $revObjs[$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' => $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( !$this->wasPosted ) {
- $wgOut->permissionRequired( 'suppressrevision' );
- return;
- }
- $UserAllowed = false;
+ $list = $this->getList();
+ for ( $list->reset(); $list->current(); $list->next() ) {
+ $item = $list->current();
+ if ( !$item->canView() ) {
+ if( !$this->submitClicked ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return;
}
- $revisions++;
- $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) );
- $bitfields |= $revObjs[$timestamp]->mDeleted;
+ $UserAllowed = false;
}
+ $numRevisions++;
+ $wgOut->addHTML( $item->getHTML() );
}
- if( !$revisions ) {
+
+ if( !$numRevisions ) {
$wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
return;
}
@@ -282,1235 +363,1485 @@ class SpecialRevisionDelete extends UnlistedSpecialPage {
// 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() );
+ // Show form if the user can submit
+ if( $this->mIsAllowed ) {
+ $out = Xml::openElement( 'form', array( 'method' => 'post',
+ 'action' => $this->getTitle()->getLocalUrl( array( 'action' => 'submit' ) ),
+ 'id' => 'mw-revdel-form-revisions' ) ) .
+ Xml::fieldset( wfMsg( 'revdelete-legend' ) ) .
+ $this->buildCheckBoxes() .
+ Xml::openElement( 'table' ) .
+ "<tr>\n" .
+ '<td class="mw-label">' .
+ Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) .
+ '</td>' .
+ '<td class="mw-input">' .
+ Xml::listDropDown( 'wpRevDeleteReasonList',
+ wfMsgForContent( 'revdelete-reason-dropdown' ),
+ wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1
+ ) .
+ '</td>' .
+ "</tr><tr>\n" .
+ '<td class="mw-label">' .
+ Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) .
+ '</td>' .
+ '<td class="mw-input">' .
+ Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason' ) ) .
+ '</td>' .
+ "</tr><tr>\n" .
+ '<td></td>' .
+ '<td class="mw-submit">' .
+ Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions),
+ array( 'name' => 'wpSubmit' ) ) .
+ '</td>' .
+ "</tr>\n" .
+ Xml::closeElement( 'table' ) .
+ Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
+ Xml::hidden( 'target', $this->targetObj->getPrefixedText() ) .
+ Xml::hidden( 'type', $this->typeName ) .
+ Xml::hidden( 'ids', implode( ',', $this->ids ) ) .
+ Xml::closeElement( 'fieldset' ) . "\n";
} 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' ) )
- );
-
- $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
- foreach( $items as $item ) {
- $wgOut->addHTML( Xml::tags( 'p', null, $item ) );
- }
- foreach( $hidden as $item ) {
- $wgOut->addHTML( $item );
+ $out = '';
+ }
+ if( $this->mIsAllowed ) {
+ $out .= Xml::closeElement( 'form' ) . "\n";
+ // Show link to edit the dropdown reasons
+ if( $wgUser->isAllowed( 'editinterface' ) ) {
+ $title = Title::makeTitle( NS_MEDIAWIKI, 'revdelete-reason-dropdown' );
+ $link = $wgUser->getSkin()->link(
+ $title,
+ wfMsgHtml( 'revdelete-edit-reasonlist' ),
+ array(),
+ array( 'action' => 'edit' )
+ );
+ $out .= Xml::tags( 'p', array( 'class' => 'mw-revdel-editreasons' ), $link ) . "\n";
+ }
}
- $wgOut->addHTML(
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' ) . "\n"
- );
+ $wgOut->addHTML( $out );
}
/**
- * This lets a user set restrictions for archived images
+ * Show some introductory text
+ * FIXME Wikimedia-specific policy text
*/
- private function showImages() {
- global $wgOut, $wgUser, $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_MASTER );
- // 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[] = $timestamp.'!'.$this->page->getDBKey();
- }
- $result = $dbr->select( 'oldimage', '*',
- array(
- 'oi_name' => $this->page->getDBKey(),
- 'oi_archive_name' => $where ),
- __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;
+ protected function addUsageText() {
+ global $wgOut, $wgUser;
+ $wgOut->addWikiMsg( 'revdelete-text' );
+ if( $wgUser->isAllowed( 'suppressrevision' ) ) {
+ $wgOut->addWikiMsg( 'revdelete-suppress-text' );
+ }
+ if( $this->mIsAllowed ) {
+ $wgOut->addWikiMsg( 'revdelete-confirm' );
+ }
+ }
+
+ /**
+ * @return String: HTML
+ */
+ protected function buildCheckBoxes() {
+ global $wgRequest;
+
+ $html = '<table>';
+ // If there is just one item, use checkboxes
+ $list = $this->getList();
+ if( $list->length() == 1 ) {
+ $list->reset();
+ $bitfield = $list->current()->getBits(); // existing field
+ if( $this->submitClicked ) {
+ $bitfield = $this->extractBitfield( $this->extractBitParams($wgRequest), $bitfield );
}
- // 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( !$this->wasPosted ) {
- $wgOut->permissionRequired( 'suppressrevision' );
- return;
- }
- $UserAllowed = false;
- }
- $revisions++;
- // Inject history info
- $wgOut->addHTML( $this->fileLine( $filesObjs[$archivename] ) );
- $bitfields |= $filesObjs[$archivename]->deleted;
+ foreach( $this->checks as $item ) {
+ list( $message, $name, $field ) = $item;
+ $innerHTML = Xml::checkLabel( wfMsg($message), $name, $name, $bitfield & $field );
+ if( $field == Revision::DELETED_RESTRICTED )
+ $innerHTML = "<b>$innerHTML</b>";
+ $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML );
+ $html .= "<tr>$line</tr>\n";
}
- // Archived files...
+ // Otherwise, use tri-state radios
} else {
- // Run through and pull all our data in one query
- foreach( $this->afiles as $id ) {
- $where[] = intval($id);
- }
- $result = $dbr->select( 'filearchive', '*',
- array(
- 'fa_name' => $this->page->getDBKey(),
- 'fa_id' => $where ),
- __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( !$this->wasPosted ) {
- $wgOut->permissionRequired( 'suppressrevision' );
- return;
- }
- $UserAllowed = false;
+ $html .= '<tr>';
+ $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-same').'</th>';
+ $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-unset').'</th>';
+ $html .= '<th class="mw-revdel-checkbox">'.wfMsgHtml('revdelete-radio-set').'</th>';
+ $html .= "<th></th></tr>\n";
+ foreach( $this->checks as $item ) {
+ list( $message, $name, $field ) = $item;
+ // If there are several items, use third state by default...
+ if( $this->submitClicked ) {
+ $selected = $wgRequest->getInt( $name, 0 /* unchecked */ );
+ } else {
+ $selected = -1; // use existing field
+ }
+ $line = '<td class="mw-revdel-checkbox">' . Xml::radio( $name, -1, $selected == -1 ) . '</td>';
+ $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 0, $selected == 0 ) . '</td>';
+ $line .= '<td class="mw-revdel-checkbox">' . Xml::radio( $name, 1, $selected == 1 ) . '</td>';
+ $label = wfMsgHtml($message);
+ if( $field == Revision::DELETED_RESTRICTED ) {
+ $label = "<b>$label</b>";
}
- $revisions++;
- // Inject history info
- $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) );
- $bitfields |= $filesObjs[$fileid]->deleted;
+ $line .= "<td>$label</td>";
+ $html .= "<tr>$line</tr>\n";
}
}
- if( !$revisions ) {
- $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' );
- return;
- }
- $wgOut->addHTML( "</ul>" );
- // Explanation text
- $this->addUsageText();
- // 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' ) )
- );
+ $html .= '</table>';
+ return $html;
+ }
- $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
- foreach( $items as $item ) {
- $wgOut->addHTML( "<p>$item</p>" );
+ /**
+ * UI entry point for form submission.
+ * @param $request WebRequest
+ */
+ protected function submit( $request ) {
+ global $wgUser, $wgOut;
+ # Check edit token on submission
+ if( $this->submitClicked && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) {
+ $wgOut->addWikiMsg( 'sessionfailure' );
+ return false;
}
- foreach( $hidden as $item ) {
- $wgOut->addHTML( $item );
+ $bitParams = $this->extractBitParams( $request );
+ $listReason = $request->getText( 'wpRevDeleteReasonList', 'other' ); // from dropdown
+ $comment = $listReason;
+ if( $comment != 'other' && $this->otherReason != '' ) {
+ // Entry from drop down menu + additional comment
+ $comment .= wfMsgForContent( 'colon-separator' ) . $this->otherReason;
+ } elseif( $comment == 'other' ) {
+ $comment = $this->otherReason;
}
-
- $wgOut->addHTML(
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' ) . "\n"
- );
+ # Can the user set this field?
+ if( $bitParams[Revision::DELETED_RESTRICTED]==1 && !$wgUser->isAllowed('suppressrevision') ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ return false;
+ }
+ # If the save went through, go to success message...
+ $status = $this->save( $bitParams, $comment, $this->targetObj );
+ if ( $status->isGood() ) {
+ $this->success();
+ return true;
+ # ...otherwise, bounce back to form...
+ } else {
+ $this->failure( $status );
+ }
+ return false;
}
/**
- * This lets a user set restrictions for log items
+ * Report that the submit operation succeeded
*/
- private function showLogItems() {
- global $wgOut, $wgUser, $wgMessageCache, $wgLang;
- $UserAllowed = true;
-
- $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) );
+ protected function success() {
+ global $wgOut;
+ $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
+ $wgOut->wrapWikiMsg( '<span class="success">$1</span>', $this->typeInfo['success'] );
+ $this->list->reloadFromMaster();
+ $this->showForm();
+ }
- $bitfields = 0;
- $wgOut->addHTML( "<ul>" );
+ /**
+ * Report that the submit operation failed
+ */
+ protected function failure( $status ) {
+ global $wgOut;
+ $wgOut->setPagetitle( wfMsg( 'actionfailed' ) );
+ $wgOut->addWikiText( $status->getWikiText( $this->typeInfo['failure'] ) );
+ $this->showForm();
+ }
- $where = $logRows = array();
- $dbr = wfGetDB( DB_MASTER );
- // Run through and pull all our data in one query
- $logItems = 0;
- foreach( $this->events as $logid ) {
- $where[] = intval($logid);
+ /**
+ * Put together an array that contains -1, 0, or the *_deleted const for each bit
+ * @param $request WebRequest
+ * @return array
+ */
+ protected function extractBitParams( $request ) {
+ $bitfield = array();
+ foreach( $this->checks as $item ) {
+ list( /* message */ , $name, $field ) = $item;
+ $val = $request->getInt( $name, 0 /* unchecked */ );
+ if( $val < -1 || $val > 1) {
+ $val = -1; // -1 for existing value
+ }
+ $bitfield[$field] = $val;
}
- list($log,$logtype) = explode( '/',$this->page->getDBKey(), 2 );
- $result = $dbr->select( 'logging', '*',
- array(
- 'log_type' => $logtype,
- 'log_id' => $where ),
- __METHOD__ );
- while( $row = $dbr->fetchObject( $result ) ) {
- $logRows[$row->log_id] = $row;
+ if( !isset($bitfield[Revision::DELETED_RESTRICTED]) ) {
+ $bitfield[Revision::DELETED_RESTRICTED] = 0;
}
- $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( !$this->wasPosted ) {
- $wgOut->permissionRequired( 'suppressrevision' );
- return;
- }
- $UserAllowed = false;
+ return $bitfield;
+ }
+
+ /**
+ * Put together a rev_deleted bitfield
+ * @param $bitPars array extractBitParams() params
+ * @param $oldfield int current bitfield
+ * @return array
+ */
+ public static function extractBitfield( $bitPars, $oldfield ) {
+ // Build the actual new rev_deleted bitfield
+ $newBits = 0;
+ foreach( $bitPars as $const => $val ) {
+ if( $val == 1 ) {
+ $newBits |= $const; // $const is the *_deleted const
+ } else if( $val == -1 ) {
+ $newBits |= ($oldfield & $const); // use existing
}
- $logItems++;
- $wgOut->addHTML( $this->logLine( $logRows[$logid] ) );
- $bitfields |= $logRows[$logid]->log_deleted;
- }
- if( !$logItems ) {
- $wgOut->showErrorPage( 'revdelete-nologid-title', 'revdelete-nologid-text' );
- return;
}
-
- $wgOut->addHTML( "</ul>" );
- // Explanation text
- $this->addUsageText();
- // Normal sysops can always see what they did, but can't always change it
- if( !$UserAllowed ) return;
+ return $newBits;
+ }
- $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' ) )
+ /**
+ * Do the write operations. Simple wrapper for RevDel_*List::setVisibility().
+ */
+ protected function save( $bitfield, $reason, $title ) {
+ return $this->getList()->setVisibility(
+ array( 'value' => $bitfield, 'comment' => $reason )
);
-
- $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) );
- foreach( $items as $item ) {
- $wgOut->addHTML( "<p>$item</p>" );
+ }
+}
+
+/**
+ * Temporary b/c interface, collection of static functions.
+ * @ingroup SpecialPage
+ */
+class RevisionDeleter {
+ /**
+ * Checks for a change in the bitfield for a certain option and updates the
+ * provided array accordingly.
+ *
+ * @param $desc String: description to add to the array if the option was
+ * enabled / disabled.
+ * @param $field Integer: the bitmask describing the single option.
+ * @param $diff Integer: the xor of the old and new bitfields.
+ * @param $new Integer: the new bitfield
+ * @param $arr Array: the array to update.
+ */
+ protected static 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 $n Integer: the new bitfield.
+ * @param $o Integer: the old bitfield.
+ * @return An array as described above.
+ */
+ protected static function getChanges( $n, $o ) {
+ $diff = $n ^ $o;
+ $ret = array( 0 => array(), 1 => array(), 2 => array() );
+ // Build bitfield changes in language
+ self::checkItem( wfMsgForContent( 'revdelete-content' ),
+ Revision::DELETED_TEXT, $diff, $n, $ret );
+ self::checkItem( wfMsgForContent( 'revdelete-summary' ),
+ Revision::DELETED_COMMENT, $diff, $n, $ret );
+ self::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' );
}
- foreach( $hidden as $item ) {
- $wgOut->addHTML( $item );
+ 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 $count Integer: The number of effected revisions.
+ * @param $nbitfield Integer: The new bitfield for the revision.
+ * @param $obitfield Integer: The old bitfield for the revision.
+ * @param $isForLog Boolean
+ */
+ public static function getLogMessage( $count, $nbitfield, $obitfield, $isForLog = false ) {
+ global $wgLang;
+ $s = '';
+ $changes = self::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] ) ) {
+ $s .= $s ? ' (' . $changes[2][0] . ')' : $changes[2][0];
+ }
+ $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
+ return wfMsgExt( $msg, array( 'parsemag', 'content' ), $s, $wgLang->formatNum($count) );
- $wgOut->addHTML(
- Xml::closeElement( 'fieldset' ) .
- Xml::closeElement( 'form' ) . "\n"
- );
}
- private function addUsageText() {
- global $wgOut, $wgUser;
- $wgOut->addWikiMsg( 'revdelete-text' );
- if( $wgUser->isAllowed( 'suppressrevision' ) ) {
- $wgOut->addWikiMsg( 'revdelete-suppress-text' );
+ // Get DB field name for URL param...
+ // Future code for other things may also track
+ // other types of revision-specific changes.
+ // @returns string One of log_id/rev_id/fa_id/ar_timestamp/oi_archive_name
+ public static function getRelationType( $typeName ) {
+ if ( isset( SpecialRevisionDelete::$deprecatedTypeMap[$typeName] ) ) {
+ $typeName = SpecialRevisionDelete::$deprecatedTypeMap[$typeName];
+ }
+ if ( isset( SpecialRevisionDelete::$allowedTypes[$typeName] ) ) {
+ $class = SpecialRevisionDelete::$allowedTypes[$typeName]['list-class'];
+ $list = new $class( null, null, null );
+ return $list->getIdField();
+ } else {
+ return null;
}
}
-
+}
+
+/**
+ * Abstract base class for a list of deletable items
+ */
+abstract class RevDel_List {
+ var $special, $title, $ids, $res, $current;
+ var $type = null; // override this
+ var $idField = null; // override this
+ var $dateField = false; // override this
+ var $authorIdField = false; // override this
+ var $authorNameField = false; // override this
+
/**
- * @param int $bitfields, aggregate bitfield of all the bitfields
- * @returns string HTML
- */
- private function buildCheckBoxes( $bitfields ) {
- $html = '';
- // 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;
- $line = Xml::tags( 'div', null, Xml::checkLabel( wfMsg($message), $name, $name,
- $bitfields & $field ) );
- if( $field == Revision::DELETED_RESTRICTED ) $line = "<b>$line</b>";
- $html .= $line;
- }
- return $html;
+ * @param $special The parent SpecialPage
+ * @param $title The target title
+ * @param $ids Array of IDs
+ */
+ public function __construct( $special, $title, $ids ) {
+ $this->special = $special;
+ $this->title = $title;
+ $this->ids = $ids;
}
/**
- * @param Revision $rev
- * @returns string
+ * Get the internal type name of this list. Equal to the table name.
*/
- private function historyLine( $rev ) {
- global $wgLang, $wgUser;
+ public function getType() {
+ return $this->type;
+ }
- $date = $wgLang->timeanddate( $rev->getTimestamp() );
- $difflink = $del = '';
- // Live revisions
- if( $this->deleteKey=='oldid' ) {
- $tokenParams = '&unhide=1&token='.urlencode( $wgUser->editToken( $rev->getId() ) );
- $revlink = $this->skin->makeLinkObj( $this->page, $date, 'oldid='.$rev->getId() . $tokenParams );
- $difflink = '(' . $this->skin->makeKnownLinkObj( $this->page, wfMsgHtml('diff'),
- 'diff=' . $rev->getId() . '&oldid=prev' . $tokenParams ) . ')';
- // 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() ) . ')';
- }
- // Check permissions; items may be "suppressed"
- 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') . ')';
- }
- }
- $userlink = $this->skin->revUserLink( $rev );
- $comment = $this->skin->revComment( $rev );
+ /**
+ * Get the DB field name associated with the ID list
+ */
+ public function getIdField() {
+ return $this->idField;
+ }
- return "<li>$difflink $revlink $userlink $comment{$del}</li>";
+ /**
+ * Get the DB field name storing timestamps
+ */
+ public function getTimestampField() {
+ return $this->dateField;
}
/**
- * @param File $file
- * @returns string
+ * Get the DB field name storing user ids
*/
- private function fileLine( $file ) {
- global $wgLang, $wgTitle;
+ public function getAuthorIdField() {
+ return $this->authorIdField;
+ }
- $target = $this->page->getPrefixedText();
- $date = $wgLang->timeanddate( $file->getTimestamp(), true );
+ /**
+ * Get the DB field name storing user names
+ */
+ public function getAuthorNameField() {
+ return $this->authorNameField;
+ }
+ /**
+ * Set the visibility for the revisions in this list. Logging and
+ * transactions are done here.
+ *
+ * @param $params Associative array of parameters. Members are:
+ * value: The integer value to set the visibility to
+ * comment: The log comment.
+ * @return Status
+ */
+ public function setVisibility( $params ) {
+ $bitPars = $params['value'];
+ $comment = $params['comment'];
- $del = '';
- # Hidden files...
- if( $file->isDeleted(File::DELETED_FILE) ) {
- $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
- if( !$file->userCan(File::DELETED_FILE) ) {
- $pageLink = $date;
+ $this->res = false;
+ $dbw = wfGetDB( DB_MASTER );
+ $this->doQuery( $dbw );
+ $dbw->begin();
+ $status = Status::newGood();
+ $missing = array_flip( $this->ids );
+ $this->clearFileOps();
+ $idsForLog = array();
+ $authorIds = $authorIPs = array();
+
+ for ( $this->reset(); $this->current(); $this->next() ) {
+ $item = $this->current();
+ unset( $missing[ $item->getId() ] );
+
+ $oldBits = $item->getBits();
+ // Build the actual new rev_deleted bitfield
+ $newBits = SpecialRevisionDelete::extractBitfield( $bitPars, $oldBits );
+
+ if ( $oldBits == $newBits ) {
+ $status->warning( 'revdelete-no-change', $item->formatDate(), $item->formatTime() );
+ $status->failCount++;
+ continue;
+ } elseif ( $oldBits == 0 && $newBits != 0 ) {
+ $opType = 'hide';
+ } elseif ( $oldBits != 0 && $newBits == 0 ) {
+ $opType = 'show';
} else {
- $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date,
- "target=$target&file=$file->sha1.".$file->getExtension() );
+ $opType = 'modify';
+ }
+
+ if ( $item->isHideCurrentOp( $newBits ) ) {
+ // Cannot hide current version text
+ $status->error( 'revdelete-hide-current', $item->formatDate(), $item->formatTime() );
+ $status->failCount++;
+ continue;
+ }
+ if ( !$item->canView() ) {
+ // Cannot access this revision
+ $msg = ($opType == 'show') ?
+ 'revdelete-show-no-access' : 'revdelete-modify-no-access';
+ $status->error( $msg, $item->formatDate(), $item->formatTime() );
+ $status->failCount++;
+ continue;
+ }
+ // Cannot just "hide from Sysops" without hiding any fields
+ if( $newBits == Revision::DELETED_RESTRICTED ) {
+ $status->warning( 'revdelete-only-restricted', $item->formatDate(), $item->formatTime() );
+ $status->failCount++;
+ continue;
+ }
+
+ // Update the revision
+ $ok = $item->setBits( $newBits );
+
+ if ( $ok ) {
+ $idsForLog[] = $item->getId();
+ $status->successCount++;
+ if( $item->getAuthorId() > 0 ) {
+ $authorIds[] = $item->getAuthorId();
+ } else if( IP::isIPAddress( $item->getAuthorName() ) ) {
+ $authorIPs[] = $item->getAuthorName();
+ }
+ } else {
+ $status->error( 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() );
+ $status->failCount++;
}
- $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 );
+ // Handle missing revisions
+ foreach ( $missing as $id => $unused ) {
+ $status->error( 'revdelete-modify-missing', $id );
+ $status->failCount++;
+ }
- return "<li>$pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
- }
+ if ( $status->successCount == 0 ) {
+ $status->ok = false;
+ $dbw->rollback();
+ return $status;
+ }
- /**
- * @param ArchivedFile $file
- * @returns string
- */
- private function archivedfileLine( $file ) {
- global $wgLang;
+ // Save success count
+ $successCount = $status->successCount;
- $target = $this->page->getPrefixedText();
- $date = $wgLang->timeanddate( $file->getTimestamp(), true );
+ // Move files, if there are any
+ $status->merge( $this->doPreCommitUpdates() );
+ if ( !$status->isOK() ) {
+ // Fatal error, such as no configured archive directory
+ $dbw->rollback();
+ return $status;
+ }
- $undelete = SpecialPage::getTitleFor( 'Undelete' );
- $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" );
+ // Log it
+ $this->updateLog( array(
+ 'title' => $this->title,
+ 'count' => $successCount,
+ 'newBits' => $newBits,
+ 'oldBits' => $oldBits,
+ 'comment' => $comment,
+ 'ids' => $idsForLog,
+ 'authorIds' => $authorIds,
+ 'authorIPs' => $authorIPs
+ ) );
+ $dbw->commit();
- $del = '';
- if( $file->isDeleted(File::DELETED_FILE) ) {
- $del = ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>';
- }
+ // Clear caches
+ $status->merge( $this->doPostCommitUpdates() );
+ return $status;
+ }
- $data = wfMsg( 'widthheight',
- $wgLang->formatNum( $file->getWidth() ),
- $wgLang->formatNum( $file->getHeight() ) ) .
- ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')';
- $data = htmlspecialchars( $data );
+ /**
+ * Reload the list data from the master DB. This can be done after setVisibility()
+ * to allow $item->getHTML() to show the new data.
+ */
+ function reloadFromMaster() {
+ $dbw = wfGetDB( DB_MASTER );
+ $this->res = $this->doQuery( $dbw );
+ }
- return "<li> $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del</li>";
+ /**
+ * Record a log entry on the action
+ * @param $params Associative array of parameters:
+ * newBits: The new value of the *_deleted bitfield
+ * oldBits: The old value of the *_deleted bitfield.
+ * title: The target title
+ * ids: The ID list
+ * comment: The log comment
+ * authorsIds: The array of the user IDs of the offenders
+ * authorsIPs: The array of the IP/anon user offenders
+ */
+ protected function updateLog( $params ) {
+ // Get the URL param's corresponding DB field
+ $field = RevisionDeleter::getRelationType( $this->getType() );
+ if( !$field ) {
+ throw new MWException( "Bad log URL param type!" );
+ }
+ // Put things hidden from sysops in the oversight log
+ if ( ( $params['newBits'] | $params['oldBits'] ) & $this->getSuppressBit() ) {
+ $logType = 'suppress';
+ } else {
+ $logType = 'delete';
+ }
+ // Add params for effected page and ids
+ $logParams = $this->getLogParams( $params );
+ // Actually add the deletion log entry
+ $log = new LogPage( $logType );
+ $logid = $log->addEntry( $this->getLogAction(), $params['title'],
+ $params['comment'], $logParams );
+ // Allow for easy searching of deletion log items for revision/log items
+ $log->addRelations( $field, $params['ids'], $logid );
+ $log->addRelations( 'target_author_id', $params['authorIds'], $logid );
+ $log->addRelations( 'target_author_ip', $params['authorIPs'], $logid );
}
/**
- * @param Array $row row
- * @returns string
+ * Get the log action for this list type
*/
- private function logLine( $row ) {
- global $wgLang;
+ public function getLogAction() {
+ return 'revision';
+ }
- $date = $wgLang->timeanddate( $row->log_timestamp );
- $paramArray = LogPage::extractParams( $row->log_params );
- $title = Title::makeTitle( $row->log_namespace, $row->log_title );
+ /**
+ * Get log parameter array.
+ * @param $params Associative array of log parameters, same as updateLog()
+ * @return array
+ */
+ public function getLogParams( $params ) {
+ return array(
+ $this->getType(),
+ implode( ',', $params['ids'] ),
+ "ofield={$params['oldBits']}",
+ "nfield={$params['newBits']}"
+ );
+ }
- $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>';
+ /**
+ * Initialise the current iteration pointer
+ */
+ protected function initCurrent() {
+ $row = $this->res->current();
+ if ( $row ) {
+ $this->current = $this->newItem( $row );
} 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>';
+ $this->current = false;
}
- // 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
+ * Start iteration. This must be called before current() or next().
+ * @return First list item
*/
- 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 );
+ public function reset() {
+ if ( !$this->res ) {
+ $this->res = $this->doQuery( wfGetDB( DB_SLAVE ) );
} else {
- $link = wfMsgHtml( 'rev-deleted-user' );
- }
- if( $file->isDeleted( Revision::DELETED_USER ) ) {
- return '<span class="history-deleted">' . $link . '</span>';
+ $this->res->rewind();
}
- return $link;
+ $this->initCurrent();
+ return $this->current;
}
/**
- * Wrap and format the given file's comment block, if the current
- * user is allowed to view it.
- *
- * @param ArchivedFile $file
- * @return string HTML
+ * Get the current list item, or false if we are at the end
*/
- 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;
+ public function current() {
+ return $this->current;
}
/**
- * @param WebRequest $request
+ * Move the iteration pointer to the next list item, and return it.
*/
- private function submit( $request ) {
- global $wgUser, $wgOut;
- # Check edit token on submission
- if( $this->wasPosted && !$wgUser->matchEditToken( $request->getVal('wpEditToken') ) ) {
- $wgOut->addWikiMsg( 'sessionfailure' );
- return false;
- }
- $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();
+ public function next() {
+ $this->res->next();
+ $this->initCurrent();
+ return $this->current;
+ }
+
+ /**
+ * Get the number of items in the list.
+ */
+ public function length() {
+ if( !$this->res ) {
+ return 0;
+ } else {
+ return $this->res->numRows();
}
}
- private function success() {
- global $wgOut;
-
- $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
-
- $wrap = '<span class="success">$1</span>';
+ /**
+ * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates()
+ * STUB
+ */
+ public function clearFileOps() {
+ }
- 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();
- }
+ /**
+ * A hook for setVisibility(): do batch updates pre-commit.
+ * STUB
+ * @return Status
+ */
+ public function doPreCommitUpdates() {
+ return Status::newGood();
}
/**
- * Put together a rev_deleted bitfield from the submitted checkboxes
- * @param WebRequest $request
- * @return int
+ * A hook for setVisibility(): do any necessary updates post-commit.
+ * STUB
+ * @return Status
*/
- private function extractBitfield( $request ) {
- $bitfield = 0;
- foreach( $this->checks as $item ) {
- list( /* message */ , $name, $field ) = $item;
- if( $request->getCheck( $name ) ) {
- $bitfield |= $field;
- }
- }
- return $bitfield;
+ public function doPostCommitUpdates() {
+ return Status::newGood();
}
- 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 );
- }
- }
+ /**
+ * Create an item object from a DB result row
+ * @param $row stdclass
+ */
+ abstract public function newItem( $row );
+
+ /**
+ * Do the DB query to iterate through the objects.
+ * @param $db Database object to use for the query
+ */
+ abstract public function doQuery( $db );
+
+ /**
+ * Get the integer value of the flag used for suppression
+ */
+ abstract public function getSuppressBit();
}
/**
- * Implements the actions for Revision Deletion.
- * @ingroup SpecialPage
+ * Abstract base class for deletable items
*/
-class RevisionDeleter {
- function __construct( $db ) {
- $this->dbw = $db;
+abstract class RevDel_Item {
+ /** The parent SpecialPage */
+ var $special;
+
+ /** The parent RevDel_List */
+ var $list;
+
+ /** The DB result row */
+ var $row;
+
+ /**
+ * @param $list RevDel_List
+ * @param $row DB result row
+ */
+ public function __construct( $list, $row ) {
+ $this->special = $list->special;
+ $this->list = $list;
+ $this->row = $row;
}
/**
- * @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
+ * Get the ID, as it would appear in the ids URL parameter
*/
- function setRevVisibility( $title, $items, $bitfield, $comment ) {
- global $wgOut;
+ public function getId() {
+ $field = $this->list->getIdField();
+ return $this->row->$field;
+ }
- $userAllowedAll = $success = true;
- $revIDs = array();
- $revCount = 0;
- // Run through and pull all our data in one query
- foreach( $items as $revid ) {
- $where[] = intval($revid);
- }
- $result = $this->dbw->select( 'revision', '*',
- array(
- 'rev_page' => $title->getArticleID(),
- 'rev_id' => $where ),
- __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;
+ /**
+ * Get the date, formatted with $wgLang
+ */
+ public function formatDate() {
+ global $wgLang;
+ return $wgLang->date( $this->getTimestamp() );
+ }
- $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;
- }
+ /**
+ * Get the time, formatted with $wgLang
+ */
+ public function formatTime() {
+ global $wgLang;
+ return $wgLang->time( $this->getTimestamp() );
+ }
- return $success;
+ /**
+ * Get the timestamp in MW 14-char form
+ */
+ public function getTimestamp() {
+ $field = $this->list->getTimestampField();
+ return wfTimestamp( TS_MW, $this->row->$field );
+ }
+
+ /**
+ * Get the author user ID
+ */
+ public function getAuthorId() {
+ $field = $this->list->getAuthorIdField();
+ return intval( $this->row->$field );
+ }
+
+ /**
+ * Get the author user name
+ */
+ public function getAuthorName() {
+ $field = $this->list->getAuthorNameField();
+ return strval( $this->row->$field );
}
- /**
- * @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
+ /**
+ * Returns true if the item is "current", and the operation to set the given
+ * bits can't be executed for that reason
+ * STUB
*/
- function setArchiveVisibility( $title, $items, $bitfield, $comment ) {
- global $wgOut;
+ public function isHideCurrentOp( $newBits ) {
+ return false;
+ }
- $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->timestamp( $timestamp );
- }
- $result = $this->dbw->select( 'archive', '*',
- array(
- 'ar_namespace' => $title->getNamespace(),
- 'ar_title' => $title->getDBKey(),
- 'ar_timestamp' => $where ),
- __METHOD__ );
- while( $row = $this->dbw->fetchObject( $result ) ) {
- $timestamp = wfTimestamp( TS_MW, $row->ar_timestamp );
- $revObjs[$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' => $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++;
+ /**
+ * Returns true if the current user can view the item
+ */
+ abstract public function canView();
+
+ /**
+ * Returns true if the current user can view the item text/file
+ */
+ abstract public function canViewContent();
- $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;
- }
+ /**
+ * Get the current deletion bitfield value
+ */
+ abstract public function getBits();
- return $success;
- }
+ /**
+ * Get the HTML of the list item. Should be include <li></li> tags.
+ * This is used to show the list in HTML form, by the special page.
+ */
+ abstract public function getHTML();
- /**
- * @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
+ /**
+ * Set the visibility of the item. This should do any necessary DB queries.
+ *
+ * The DB update query should have a condition which forces it to only update
+ * if the value in the DB matches the value fetched earlier with the SELECT.
+ * If the update fails because it did not match, the function should return
+ * false. This prevents concurrency problems.
+ *
+ * @return boolean success
*/
- function setOldImgVisibility( $title, $items, $bitfield, $comment ) {
- global $wgOut;
+ abstract public function setBits( $newBits );
+}
- $userAllowedAll = $success = true;
- $count = 0;
- $set = array();
- // Run through and pull all our data in one query
- foreach( $items as $timestamp ) {
- $where[] = $timestamp.'!'.$title->getDBKey();
- }
- $result = $this->dbw->select( 'oldimage', '*',
+/**
+ * List for revision table items
+ */
+class RevDel_RevisionList extends RevDel_List {
+ var $currentRevId;
+ var $type = 'revision';
+ var $idField = 'rev_id';
+ var $dateField = 'rev_timestamp';
+ var $authorIdField = 'rev_user';
+ var $authorNameField = 'rev_user_text';
+
+ public function doQuery( $db ) {
+ $ids = array_map( 'intval', $this->ids );
+ return $db->select( array('revision','page'), '*',
array(
- 'oi_name' => $title->getDBKey(),
- 'oi_archive_name' => $where ),
- __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;
- }
+ 'rev_page' => $this->title->getArticleID(),
+ 'rev_id' => $ids,
+ 'rev_page = page_id'
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'rev_id DESC' )
+ );
+ }
- $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();
- }
- }
+ public function newItem( $row ) {
+ return new RevDel_RevisionItem( $this, $row );
+ }
- // 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;
+ public function getCurrent() {
+ if ( is_null( $this->currentRevId ) ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $this->currentRevId = $dbw->selectField(
+ 'page', 'page_latest', $this->title->pageCond(), __METHOD__ );
}
-
- return $success;
+ return $this->currentRevId;
}
- /**
- * @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;
+ public function getSuppressBit() {
+ return Revision::DELETED_RESTRICTED;
+ }
- $userAllowedAll = $success = true;
- $count = 0;
- $Id_set = array();
+ public function doPreCommitUpdates() {
+ $this->title->invalidateCache();
+ return Status::newGood();
+ }
- // Run through and pull all our data in one query
- foreach( $items as $id ) {
- $where[] = intval($id);
- }
- $result = $this->dbw->select( 'filearchive', '*',
- array( 'fa_name' => $title->getDBKey(),
- 'fa_id' => $where ),
- __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++;
+ public function doPostCommitUpdates() {
+ $this->title->purgeSquid();
+ // Extensions that require referencing previous revisions may need this
+ wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$this->title ) );
+ return Status::newGood();
+ }
+}
- $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;
- }
+/**
+ * Item class for a revision table row
+ */
+class RevDel_RevisionItem extends RevDel_Item {
+ var $revision;
- return $success;
+ public function __construct( $list, $row ) {
+ parent::__construct( $list, $row );
+ $this->revision = new Revision( $row );
}
- /**
- * @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;
+ public function canView() {
+ return $this->revision->userCan( Revision::DELETED_RESTRICTED );
+ }
+
+ public function canViewContent() {
+ return $this->revision->userCan( Revision::DELETED_TEXT );
+ }
- $userAllowedAll = $success = true;
- $count = 0;
- $log_Ids = array();
+ public function getBits() {
+ return $this->revision->mDeleted;
+ }
- // Run through and pull all our data in one query
- foreach( $items as $logid ) {
- $where[] = intval($logid);
+ public function setBits( $bits ) {
+ $dbw = wfGetDB( DB_MASTER );
+ // Update revision table
+ $dbw->update( 'revision',
+ array( 'rev_deleted' => $bits ),
+ array(
+ 'rev_id' => $this->revision->getId(),
+ 'rev_page' => $this->revision->getPage(),
+ 'rev_deleted' => $this->getBits()
+ ),
+ __METHOD__
+ );
+ if ( !$dbw->affectedRows() ) {
+ // Concurrent fail!
+ return false;
}
- list($log,$logtype) = explode( '/',$title->getDBKey(), 2 );
- $result = $this->dbw->select( 'logging', '*',
+ // Update recentchanges table
+ $dbw->update( 'recentchanges',
+ array(
+ 'rc_deleted' => $bits,
+ 'rc_patrolled' => 1
+ ),
array(
- 'log_type' => $logtype,
- 'log_id' => $where ),
- __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++;
+ 'rc_this_oldid' => $this->revision->getId(), // condition
+ // non-unique timestamp index
+ 'rc_timestamp' => $dbw->timestamp( $this->revision->getTimestamp() ),
+ ),
+ __METHOD__
+ );
+ return true;
+ }
- $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;
- }
+ public function isDeleted() {
+ return $this->revision->isDeleted( Revision::DELETED_TEXT );
+ }
- return $success;
+ public function isHideCurrentOp( $newBits ) {
+ return ( $newBits & Revision::DELETED_TEXT )
+ && $this->list->getCurrent() == $this->getId();
}
/**
- * 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
+ * Get the HTML link to the revision text.
+ * Overridden by RevDel_ArchiveItem.
*/
- 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
- }
+ protected function getRevisionLink() {
+ global $wgLang;
+ $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true );
+ if ( $this->isDeleted() && !$this->canViewContent() ) {
+ return $date;
+ }
+ return $this->special->skin->link(
+ $this->list->title,
+ $date,
+ array(),
+ array(
+ 'oldid' => $this->revision->getId(),
+ 'unhide' => 1
+ )
+ );
+ }
+
+ /**
+ * Get the HTML link to the diff.
+ * Overridden by RevDel_ArchiveItem
+ */
+ protected function getDiffLink() {
+ if ( $this->isDeleted() && !$this->canViewContent() ) {
+ return wfMsgHtml('diff');
} else {
- wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" );
- $group = null;
- $key = '';
- $transaction = new FSTransaction(); // empty
+ return
+ $this->special->skin->link(
+ $this->list->title,
+ wfMsgHtml('diff'),
+ array(),
+ array(
+ 'diff' => $this->revision->getId(),
+ 'oldid' => 'prev',
+ 'unhide' => 1
+ ),
+ array(
+ 'known',
+ 'noclasses'
+ )
+ );
}
+ }
- 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;
+ public function getHTML() {
+ $difflink = $this->getDiffLink();
+ $revlink = $this->getRevisionLink();
+ $userlink = $this->special->skin->revUserLink( $this->revision );
+ $comment = $this->special->skin->revComment( $this->revision );
+ if ( $this->isDeleted() ) {
+ $revlink = "<span class=\"history-deleted\">$revlink</span>";
}
+ return "<li>($difflink) $revlink $userlink $comment</li>";
+ }
+}
- wfDebug( __METHOD__.": set db items, applying file transactions\n" );
- $transaction->commit();
- FileStore::unlock();
+/**
+ * List for archive table items, i.e. revisions deleted via action=delete
+ */
+class RevDel_ArchiveList extends RevDel_RevisionList {
+ var $type = 'archive';
+ var $idField = 'ar_timestamp';
+ var $dateField = 'ar_timestamp';
+ var $authorIdField = 'ar_user';
+ var $authorNameField = 'ar_user_text';
+
+ public function doQuery( $db ) {
+ $timestamps = array();
+ foreach ( $this->ids as $id ) {
+ $timestamps[] = $db->timestamp( $id );
+ }
+ return $db->select( 'archive', '*',
+ array(
+ 'ar_namespace' => $this->title->getNamespace(),
+ 'ar_title' => $this->title->getDBkey(),
+ 'ar_timestamp' => $timestamps
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'ar_timestamp DESC' )
+ );
+ }
- $m = explode('!',$oimage->archive_name,2);
- $timestamp = $m[0];
+ public function newItem( $row ) {
+ return new RevDel_ArchiveItem( $this, $row );
+ }
- return $timestamp;
+ public function doPreCommitUpdates() {
+ return Status::newGood();
}
- /**
- * 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;
- }
+ public function doPostCommitUpdates() {
+ return Status::newGood();
+ }
+}
- $store = FileStore::get( 'deleted' );
- if( !$store ) {
- wfDebug( __METHOD__.": skipping row with no file.\n" );
- return false;
+/**
+ * Item class for a archive table row
+ */
+class RevDel_ArchiveItem extends RevDel_RevisionItem {
+ public function __construct( $list, $row ) {
+ RevDel_Item::__construct( $list, $row );
+ $this->revision = Revision::newFromArchiveRow( $row,
+ array( 'page' => $this->list->title->getArticleId() ) );
+ }
+
+ public function getId() {
+ # Convert DB timestamp to MW timestamp
+ return $this->revision->getTimestamp();
+ }
+
+ public function setBits( $bits ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'archive',
+ array( 'ar_deleted' => $bits ),
+ array( 'ar_namespace' => $this->list->title->getNamespace(),
+ 'ar_title' => $this->list->title->getDBkey(),
+ // use timestamp for index
+ 'ar_timestamp' => $this->row->ar_timestamp,
+ 'ar_rev_id' => $this->row->ar_rev_id,
+ 'ar_deleted' => $this->getBits()
+ ),
+ __METHOD__ );
+ return (bool)$dbw->affectedRows();
+ }
+
+ protected function getRevisionLink() {
+ global $wgLang;
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $date = $wgLang->timeanddate( $this->revision->getTimestamp(), true );
+ if ( $this->isDeleted() && !$this->canViewContent() ) {
+ return $date;
}
+ return $this->special->skin->link( $undelete, $date, array(),
+ array(
+ 'target' => $this->list->title->getPrefixedText(),
+ 'timestamp' => $this->revision->getTimestamp()
+ ) );
+ }
- $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;
+ protected function getDiffLink() {
+ if ( $this->isDeleted() && !$this->canViewContent() ) {
+ return wfMsgHtml( 'diff' );
}
- $transaction->add( $store->export( $key, $destPath, $flags ) );
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ return $this->special->skin->link( $undelete, wfMsgHtml('diff'), array(),
+ array(
+ 'target' => $this->list->title->getPrefixedText(),
+ 'diff' => 'prev',
+ 'timestamp' => $this->revision->getTimestamp()
+ ) );
+ }
+}
- wfDebug( __METHOD__.": set db items, applying file transactions\n" );
- $transaction->commit();
- FileStore::unlock();
+/**
+ * List for oldimage table items
+ */
+class RevDel_FileList extends RevDel_List {
+ var $type = 'oldimage';
+ var $idField = 'oi_archive_name';
+ var $dateField = 'oi_timestamp';
+ var $authorIdField = 'oi_user';
+ var $authorNameField = 'oi_user_text';
+ var $storeBatch, $deleteBatch, $cleanupBatch;
+
+ public function doQuery( $db ) {
+ $archiveName = array();
+ foreach( $this->ids as $timestamp ) {
+ $archiveNames[] = $timestamp . '!' . $this->title->getDBkey();
+ }
+ return $db->select( 'oldimage', '*',
+ array(
+ 'oi_name' => $this->title->getDBkey(),
+ 'oi_archive_name' => $archiveNames
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'oi_timestamp DESC' )
+ );
+ }
- $m = explode('!',$oimage->archive_name,2);
- $timestamp = $m[0];
+ public function newItem( $row ) {
+ return new RevDel_FileItem( $this, $row );
+ }
- return $timestamp;
+ public function clearFileOps() {
+ $this->deleteBatch = array();
+ $this->storeBatch = array();
+ $this->cleanupBatch = array();
}
- /**
- * 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__ );
+ public function doPreCommitUpdates() {
+ $status = Status::newGood();
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ if ( $this->storeBatch ) {
+ $status->merge( $repo->storeBatch( $this->storeBatch, FileRepo::OVERWRITE_SAME ) );
+ }
+ if ( !$status->isOK() ) {
+ return $status;
+ }
+ if ( $this->deleteBatch ) {
+ $status->merge( $repo->deleteBatch( $this->deleteBatch ) );
+ }
+ if ( !$status->isOK() ) {
+ // Running cleanupDeletedBatch() after a failed storeBatch() with the DB already
+ // modified (but destined for rollback) causes data loss
+ return $status;
+ }
+ if ( $this->cleanupBatch ) {
+ $status->merge( $repo->cleanupDeletedBatch( $this->cleanupBatch ) );
+ }
+ return $status;
}
- /**
- * 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__ );
+ public function doPostCommitUpdates() {
+ $file = wfLocalFile( $this->title );
+ $file->purgeCache();
+ $file->purgeDescription();
+ return Status::newGood();
}
- /**
- * 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__ );
+ public function getSuppressBit() {
+ return File::DELETED_RESTRICTED;
}
+}
- /**
- * 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__ );
+/**
+ * Item class for an oldimage table row
+ */
+class RevDel_FileItem extends RevDel_Item {
+ var $file;
+
+ public function __construct( $list, $row ) {
+ parent::__construct( $list, $row );
+ $this->file = RepoGroup::singleton()->getLocalRepo()->newFileFromRow( $row );
}
- /**
- * 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__ );
+ public function getId() {
+ $parts = explode( '!', $this->row->oi_archive_name );
+ return $parts[0];
}
- /**
- * 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__ );
+ public function canView() {
+ return $this->file->userCan( File::DELETED_RESTRICTED );
+ }
+
+ public function canViewContent() {
+ return $this->file->userCan( File::DELETED_FILE );
}
- /**
- * 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__ );
+ public function getBits() {
+ return $this->file->getVisibility();
+ }
+
+ public function setBits( $bits ) {
+ # Queue the file op
+ # FIXME: move to LocalFile.php
+ if ( $this->isDeleted() ) {
+ if ( $bits & File::DELETED_FILE ) {
+ # Still deleted
+ } else {
+ # Newly undeleted
+ $key = $this->file->getStorageKey();
+ $srcRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
+ $this->list->storeBatch[] = array(
+ $this->file->repo->getVirtualUrl( 'deleted' ) . '/' . $srcRel,
+ 'public',
+ $this->file->getRel()
+ );
+ $this->list->cleanupBatch[] = $key;
+ }
+ } elseif ( $bits & File::DELETED_FILE ) {
+ # Newly deleted
+ $key = $this->file->getStorageKey();
+ $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
+ $this->list->deleteBatch[] = array( $this->file->getRel(), $dstRel );
+ }
+
+ # Do the database operations
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'oldimage',
+ array( 'oi_deleted' => $bits ),
+ array(
+ 'oi_name' => $this->row->oi_name,
+ 'oi_timestamp' => $this->row->oi_timestamp,
+ 'oi_deleted' => $this->getBits()
+ ),
+ __METHOD__
+ );
+ return (bool)$dbw->affectedRows();
+ }
+
+ public function isDeleted() {
+ return $this->file->isDeleted( File::DELETED_FILE );
}
/**
- * 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
+ * Get the link to the file.
+ * Overridden by RevDel_ArchivedFileItem.
*/
- function updatePage( $title ) {
- $title->invalidateCache();
- $title->purgeSquid();
- $title->touchLinks();
- // Extensions that require referencing previous revisions may need this
- wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) );
+ protected function getLink() {
+ global $wgLang, $wgUser;
+ $date = $wgLang->timeanddate( $this->file->getTimestamp(), true );
+ if ( $this->isDeleted() ) {
+ # Hidden files...
+ if ( !$this->canViewContent() ) {
+ $link = $date;
+ } else {
+ $link = $this->special->skin->link(
+ $this->special->getTitle(),
+ $date, array(),
+ array(
+ 'target' => $this->list->title->getPrefixedText(),
+ 'file' => $this->file->getArchiveName(),
+ 'token' => $wgUser->editToken( $this->file->getArchiveName() )
+ )
+ );
+ }
+ return '<span class="history-deleted">' . $link . '</span>';
+ } else {
+ # Regular files...
+ $url = $this->file->getUrl();
+ return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date );
+ }
}
-
/**
- * 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.
+ * Generate a user tool link cluster if the current user is allowed to view it
+ * @return string HTML
*/
- function checkItem ( $desc, $field, $diff, $new, &$arr ) {
- if ( $diff & $field ) {
- $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc;
+ protected function getUserTools() {
+ if( $this->file->userCan( Revision::DELETED_USER ) ) {
+ $link = $this->special->skin->userLink( $this->file->user, $this->file->user_text ) .
+ $this->special->skin->userToolLinks( $this->file->user, $this->file->user_text );
+ } else {
+ $link = wfMsgHtml( 'rev-deleted-user' );
}
+ if( $this->file->isDeleted( Revision::DELETED_USER ) ) {
+ return '<span class="history-deleted">' . $link . '</span>';
+ }
+ return $link;
}
/**
- * 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.
+ * Wrap and format the file's comment block, if the current
+ * user is allowed to view it.
*
- * @param int $n The new bitfield.
- * @param int $o The old bitfield.
- * @return An array as described above.
+ * @return string HTML
*/
- function getChanges ( $n, $o ) {
- $diff = $n ^ $o;
- $ret = array ( 0 => array(), 1 => array(), 2 => array() );
+ protected function getComment() {
+ if( $this->file->userCan( File::DELETED_COMMENT ) ) {
+ $block = $this->special->skin->commentBlock( $this->file->description );
+ } else {
+ $block = ' ' . wfMsgHtml( 'rev-deleted-comment' );
+ }
+ if( $this->file->isDeleted( File::DELETED_COMMENT ) ) {
+ return "<span class=\"history-deleted\">$block</span>";
+ }
+ return $block;
+ }
- $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 );
+ public function getHTML() {
+ global $wgLang;
+ $data =
+ wfMsg(
+ 'widthheight',
+ $wgLang->formatNum( $this->file->getWidth() ),
+ $wgLang->formatNum( $this->file->getHeight() )
+ ) .
+ ' (' .
+ wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $this->file->getSize() ) ) .
+ ')';
+ $pageLink = $this->getLink();
+
+ return '<li>' . $this->getLink() . ' ' . $this->getUserTools() . ' ' .
+ $data . ' ' . $this->getComment(). '</li>';
+ }
+}
- // 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' );
- }
+/**
+ * List for filearchive table items
+ */
+class RevDel_ArchivedFileList extends RevDel_FileList {
+ var $type = 'filearchive';
+ var $idField = 'fa_id';
+ var $dateField = 'fa_timestamp';
+ var $authorIdField = 'fa_user';
+ var $authorNameField = 'fa_user_text';
+
+ public function doQuery( $db ) {
+ $ids = array_map( 'intval', $this->ids );
+ return $db->select( 'filearchive', '*',
+ array(
+ 'fa_name' => $this->title->getDBkey(),
+ 'fa_id' => $ids
+ ),
+ __METHOD__,
+ array( 'ORDER BY' => 'fa_id DESC' )
+ );
+ }
- return $ret;
+ public function newItem( $row ) {
+ return new RevDel_ArchivedFileItem( $this, $row );
}
+}
- /**
- * 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;
+/**
+ * Item class for a filearchive table row
+ */
+class RevDel_ArchivedFileItem extends RevDel_FileItem {
+ public function __construct( $list, $row ) {
+ RevDel_Item::__construct( $list, $row );
+ $this->file = ArchivedFile::newFromRow( $row );
+ }
- $s = '';
- $changes = $this->getChanges( $nbitfield, $obitfield );
+ public function getId() {
+ return $this->row->fa_id;
+ }
+
+ public function setBits( $bits ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'filearchive',
+ array( 'fa_deleted' => $bits ),
+ array(
+ 'fa_id' => $this->row->fa_id,
+ 'fa_deleted' => $this->getBits(),
+ ),
+ __METHOD__
+ );
+ return (bool)$dbw->affectedRows();
+ }
- if ( count ( $changes[0] ) ) {
- $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) );
+ protected function getLink() {
+ global $wgLang, $wgUser;
+ $date = $wgLang->timeanddate( $this->file->getTimestamp(), true );
+ $undelete = SpecialPage::getTitleFor( 'Undelete' );
+ $key = $this->file->getKey();
+ # Hidden files...
+ if( !$this->canViewContent() ) {
+ $link = $date;
+ } else {
+ $link = $this->special->skin->link( $undelete, $date, array(),
+ array(
+ 'target' => $this->list->title->getPrefixedText(),
+ 'file' => $key,
+ 'token' => $wgUser->editToken( $key )
+ )
+ );
+ }
+ if( $this->isDeleted() ) {
+ $link = '<span class="history-deleted">' . $link . '</span>';
}
+ return $link;
+ }
+}
- if ( count ( $changes[1] ) ) {
- if ($s) $s .= '; ';
+/**
+ * List for logging table items
+ */
+class RevDel_LogList extends RevDel_List {
+ var $type = 'logging';
+ var $idField = 'log_id';
+ var $dateField = 'log_timestamp';
+ var $authorIdField = 'log_user';
+ var $authorNameField = 'log_user_text';
+
+ public function doQuery( $db ) {
+ global $wgMessageCache;
+ $wgMessageCache->loadAllMessages();
+ $ids = array_map( 'intval', $this->ids );
+ return $db->select( 'logging', '*',
+ array( 'log_id' => $ids ),
+ __METHOD__,
+ array( 'ORDER BY' => 'log_id DESC' )
+ );
+ }
- $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) );
- }
+ public function newItem( $row ) {
+ return new RevDel_LogItem( $this, $row );
+ }
- if ( count ( $changes[2] )) {
- if ($s)
- $s .= ' (' . $changes[2][0] . ')';
- else
- $s = $changes[2][0];
- }
+ public function getSuppressBit() {
+ return Revision::DELETED_RESTRICTED;
+ }
- $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message';
- $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ),
- $s, $wgContLang->formatNum( $count ) );
+ public function getLogAction() {
+ return 'event';
+ }
- if ( $comment )
- $ret .= ": $comment";
+ public function getLogParams( $params ) {
+ return array(
+ implode( ',', $params['ids'] ),
+ "ofield={$params['oldBits']}",
+ "nfield={$params['newBits']}"
+ );
+ }
+}
- return $ret;
+/**
+ * Item class for a logging table row
+ */
+class RevDel_LogItem extends RevDel_Item {
+ public function canView() {
+ return LogEventsList::userCan( $this->row, Revision::DELETED_RESTRICTED );
+ }
+
+ public function canViewContent() {
+ return true; // none
+ }
+ public function getBits() {
+ return $this->row->log_deleted;
}
- /**
- * 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 );
+ public function setBits( $bits ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update( 'recentchanges',
+ array(
+ 'rc_deleted' => $bits,
+ 'rc_patrolled' => 1
+ ),
+ array(
+ 'rc_logid' => $this->row->log_id,
+ 'rc_timestamp' => $this->row->log_timestamp // index
+ ),
+ __METHOD__
+ );
+ $dbw->update( 'logging',
+ array( 'log_deleted' => $bits ),
+ array(
+ 'log_id' => $this->row->log_id,
+ 'log_deleted' => $this->getBits()
+ ),
+ __METHOD__
+ );
+ return (bool)$dbw->affectedRows();
+ }
+
+ public function getHTML() {
+ global $wgLang;
- $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' );
+ $date = htmlspecialchars( $wgLang->timeanddate( $this->row->log_timestamp ) );
+ $paramArray = LogPage::extractParams( $this->row->log_params );
+ $title = Title::makeTitle( $this->row->log_namespace, $this->row->log_title );
- if( $param == 'logid' ) {
- $params = array( implode( ',', $items) );
- $log->addEntry( 'event', $title, $reason, $params );
+ // Log link for this page
+ $loglink = $this->special->skin->link(
+ SpecialPage::getTitleFor( 'Log' ),
+ wfMsgHtml( 'log' ),
+ array(),
+ array( 'page' => $title->getPrefixedText() )
+ );
+ // Action text
+ if( !$this->canView() ) {
+ $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
} else {
- // Add params for effected page and ids
- $params = array( $param, implode( ',', $items) );
- $log->addEntry( 'revision', $title, $reason, $params );
+ $action = LogPage::actionText( $this->row->log_type, $this->row->log_action, $title,
+ $this->special->skin, $paramArray, true, true );
+ if( $this->row->log_deleted & LogPage::DELETED_ACTION )
+ $action = '<span class="history-deleted">' . $action . '</span>';
}
+ // User links
+ $userLink = $this->special->skin->userLink( $this->row->log_user,
+ User::WhoIs( $this->row->log_user ) );
+ if( LogEventsList::isDeleted($this->row,LogPage::DELETED_USER) ) {
+ $userLink = '<span class="history-deleted">' . $userLink . '</span>';
+ }
+ // Comment
+ $comment = $wgLang->getDirMark() . $this->special->skin->commentBlock( $this->row->log_comment );
+ if( LogEventsList::isDeleted($this->row,LogPage::DELETED_COMMENT) ) {
+ $comment = '<span class="history-deleted">' . $comment . '</span>';
+ }
+ return "<li>($loglink) $date $userLink $action $comment</li>";
}
}
diff --git a/includes/specials/SpecialSearch.php b/includes/specials/SpecialSearch.php
index cb783819..da054e02 100644
--- a/includes/specials/SpecialSearch.php
+++ b/includes/specials/SpecialSearch.php
@@ -29,16 +29,15 @@
* @param $par String: (default '')
*/
function wfSpecialSearch( $par = '' ) {
- global $wgRequest, $wgUser, $wgUseOldSearchUI;
+ global $wgRequest, $wgUser;
// Strip underscores from title parameter; most of the time we'll want
// text form here. But don't strip underscores from actual text params!
$titleParam = str_replace( '_', ' ', $par );
// Fetch the search term
$search = str_replace( "\n", " ", $wgRequest->getText( 'search', $titleParam ) );
- $class = $wgUseOldSearchUI ? 'SpecialSearchOld' : 'SpecialSearch';
- $searchPage = new $class( $wgRequest, $wgUser );
- if( $wgRequest->getVal( 'fulltext' )
- || !is_null( $wgRequest->getVal( 'offset' ))
+ $searchPage = new SpecialSearch( $wgRequest, $wgUser );
+ if( $wgRequest->getVal( 'fulltext' )
+ || !is_null( $wgRequest->getVal( 'offset' ))
|| !is_null( $wgRequest->getVal( 'searchx' )) )
{
$searchPage->showResults( $search );
@@ -74,7 +73,7 @@ class SpecialSearch {
$this->active = 'advanced';
$this->sk = $user->getSkin();
$this->didYouMeanHtml = ''; # html of did you mean... link
- $this->fulltext = $request->getVal('fulltext');
+ $this->fulltext = $request->getVal('fulltext');
}
/**
@@ -103,7 +102,7 @@ class SpecialSearch {
wfRunHooks( 'SpecialSearchNogomatch', array( &$t ) );
# If the feature is enabled, go straight to the edit page
if( $wgGoToEdit ) {
- $wgOut->redirect( $t->getFullURL( 'action=edit' ) );
+ $wgOut->redirect( $t->getFullURL( array( 'action' => 'edit' ) ) );
return;
}
}
@@ -114,11 +113,11 @@ class SpecialSearch {
* @param string $term
*/
public function showResults( $term ) {
- global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang;
+ global $wgOut, $wgUser, $wgDisableTextSearch, $wgContLang, $wgScript;
wfProfileIn( __METHOD__ );
-
+
$sk = $wgUser->getSkin();
-
+
$this->searchEngine = SearchEngine::create();
$search =& $this->searchEngine;
$search->setLimitOffset( $this->limit, $this->offset );
@@ -126,9 +125,9 @@ class SpecialSearch {
$search->showRedirects = $this->searchRedirects;
$search->prefix = $this->mPrefix;
$term = $search->transformSearchTerm($term);
-
+
$this->setupPage( $term );
-
+
if( $wgDisableTextSearch ) {
global $wgSearchForwardUrl;
if( $wgSearchForwardUrl ) {
@@ -152,10 +151,10 @@ class SpecialSearch {
wfProfileOut( __METHOD__ );
return;
}
-
+
$t = Title::newFromText( $term );
-
- // fetch search results
+
+ // fetch search results
$rewritten = $search->replacePrefixes($term);
$titleMatches = $search->searchTitle( $rewritten );
@@ -165,95 +164,116 @@ class SpecialSearch {
// did you mean... suggestions
if( $textMatches && $textMatches->hasSuggestion() ) {
$st = SpecialPage::getTitleFor( 'Search' );
+
# mirror Go/Search behaviour of original request ..
$didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
- if($this->fulltext != NULL)
- $didYouMeanParams['fulltext'] = $this->fulltext;
- $stParams = wfArrayToCGI(
+
+ if($this->fulltext != null)
+ $didYouMeanParams['fulltext'] = $this->fulltext;
+
+ $stParams = array_merge(
$didYouMeanParams,
$this->powerSearchOptions()
);
- $suggestLink = $sk->makeKnownLinkObj( $st,
- $textMatches->getSuggestionSnippet(),
- $stParams );
+
+ $suggestionSnippet = $textMatches->getSuggestionSnippet();
+
+ if( $suggestionSnippet == '' )
+ $suggestionSnippet = null;
+
+ $suggestLink = $sk->linkKnown(
+ $st,
+ $suggestionSnippet,
+ array(),
+ $stParams
+ );
$this->didYouMeanHtml = '<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>';
}
-
- // start rendering the page
- $wgOut->addHtml(
- Xml::openElement( 'table', array( 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
+ // start rendering the page
+ $wgOut->addHtml(
+ Xml::openElement(
+ 'form',
+ array(
+ 'id' => ( $this->searchAdvanced ? 'powersearch' : 'search' ),
+ 'method' => 'get',
+ 'action' => $wgScript
+ )
+ )
+ );
+ $wgOut->addHtml(
+ Xml::openElement( 'table', array( 'id'=>'mw-search-top-table', 'border'=>0, 'cellpadding'=>0, 'cellspacing'=>0 ) ) .
Xml::openElement( 'tr' ) .
Xml::openElement( 'td' ) . "\n" .
- ( $this->searchAdvanced ? $this->powerSearchBox( $term ) : $this->shortDialog( $term ) ) .
+ $this->shortDialog( $term ) .
Xml::closeElement('td') .
Xml::closeElement('tr') .
Xml::closeElement('table')
);
-
+
// Sometimes the search engine knows there are too many hits
if( $titleMatches instanceof SearchResultTooMany ) {
$wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
wfProfileOut( __METHOD__ );
return;
}
-
+
$filePrefix = $wgContLang->getFormattedNsText(NS_FILE).':';
- if( '' === trim( $term ) || $filePrefix === trim( $term ) ) {
- $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() );
+ if( trim( $term ) === '' || $filePrefix === trim( $term ) ) {
+ $wgOut->addHTML( $this->searchFocus() );
+ $wgOut->addHTML( $this->formHeader($term, 0, 0));
+ if( $this->searchAdvanced ) {
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
+ }
+ $wgOut->addHTML( '</form>' );
// Empty query -- straight view of search form
wfProfileOut( __METHOD__ );
return;
}
- // show direct page/create link
- if( !is_null($t) ) {
- if( !$t->exists() ) {
- $wgOut->addWikiMsg( 'searchmenu-new', wfEscapeWikiText( $t->getPrefixedText() ) );
- } else {
- $wgOut->addWikiMsg( 'searchmenu-exists', wfEscapeWikiText( $t->getPrefixedText() ) );
- }
- }
-
// Get number of results
- $titleMatchesSQL = $titleMatches ? $titleMatches->numRows() : 0;
- $textMatchesSQL = $textMatches ? $textMatches->numRows() : 0;
+ $titleMatchesNum = $titleMatches ? $titleMatches->numRows() : 0;
+ $textMatchesNum = $textMatches ? $textMatches->numRows() : 0;
// Total initial query matches (possible false positives)
- $numSQL = $titleMatchesSQL + $textMatchesSQL;
+ $num = $titleMatchesNum + $textMatchesNum;
+
// Get total actual results (after second filtering, if any)
$numTitleMatches = $titleMatches && !is_null( $titleMatches->getTotalHits() ) ?
- $titleMatches->getTotalHits() : $titleMatchesSQL;
+ $titleMatches->getTotalHits() : $titleMatchesNum;
$numTextMatches = $textMatches && !is_null( $textMatches->getTotalHits() ) ?
- $textMatches->getTotalHits() : $textMatchesSQL;
- $totalRes = $numTitleMatches + $numTextMatches;
-
+ $textMatches->getTotalHits() : $textMatchesNum;
+
+ // get total number of results if backend can calculate it
+ $totalRes = 0;
+ if($titleMatches && !is_null( $titleMatches->getTotalHits() ) )
+ $totalRes += $titleMatches->getTotalHits();
+ if($textMatches && !is_null( $textMatches->getTotalHits() ))
+ $totalRes += $textMatches->getTotalHits();
+
// show number of results and current offset
- if( $numSQL > 0 ) {
- if( $numSQL > 0 ) {
- $top = wfMsgExt('showingresultstotal', array( 'parseinline' ),
- $this->offset+1, $this->offset+$numSQL, $totalRes, $numSQL );
- } elseif( $numSQL >= $this->limit ) {
- $top = wfShowingResults( $this->offset, $this->limit );
- } else {
- $top = wfShowingResultsNum( $this->offset, $this->limit, $numSQL );
- }
- $wgOut->addHTML( "<p class='mw-search-numberresults'>{$top}</p>\n" );
+ $wgOut->addHTML( $this->formHeader($term, $num, $totalRes));
+ if( $this->searchAdvanced ) {
+ $wgOut->addHTML( $this->powerSearchBox( $term ) );
}
+
+ $wgOut->addHtml( Xml::closeElement( 'form' ) );
+ $wgOut->addHtml( "<div class='searchresults'>" );
// prev/next links
- if( $numSQL || $this->offset ) {
+ if( $num || $this->offset ) {
+ // Show the create link ahead
+ $this->showCreateLink( $t );
$prevnext = wfViewPrevNext( $this->offset, $this->limit,
SpecialPage::getTitleFor( 'Search' ),
wfArrayToCGI( $this->powerSearchOptions(), array( 'search' => $term ) ),
- max( $titleMatchesSQL, $textMatchesSQL ) < $this->limit
+ max( $titleMatchesNum, $textMatchesNum ) < $this->limit
);
- $wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
+ //$wgOut->addHTML( "<p class='mw-search-pager-top'>{$prevnext}</p>\n" );
wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) );
} else {
wfRunHooks( 'SpecialSearchNoResults', array( $term ) );
- }
+ }
- $wgOut->addHtml( "<div class='searchresults'>" );
if( $titleMatches ) {
if( $numTitleMatches > 0 ) {
$wgOut->wrapWikiMsg( "==$1==\n", 'titlematches' );
@@ -265,10 +285,10 @@ class SpecialSearch {
// output appropriate heading
if( $numTextMatches > 0 && $numTitleMatches > 0 ) {
// if no title matches the heading is redundant
- $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
+ $wgOut->wrapWikiMsg( "==$1==\n", 'textmatches' );
} elseif( $totalRes == 0 ) {
# Don't show the 'no text matches' if we received title matches
- $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
+ # $wgOut->wrapWikiMsg( "==$1==\n", 'notextmatches' );
}
// show interwiki results if any
if( $textMatches->hasInterwikiResults() ) {
@@ -281,20 +301,41 @@ class SpecialSearch {
$textMatches->free();
}
- if( $totalRes === 0 ) {
- $wgOut->addWikiMsg( 'search-nonefound' );
+ if( $num === 0 ) {
+ $wgOut->addWikiMsg( 'search-nonefound', wfEscapeWikiText( $term ) );
+ $this->showCreateLink( $t );
}
$wgOut->addHtml( "</div>" );
- if( $totalRes === 0 ) {
- $wgOut->addHTML( $this->searchAdvanced ? $this->powerSearchFocus() : $this->searchFocus() );
+ if( $num === 0 ) {
+ $wgOut->addHTML( $this->searchFocus() );
}
- if( $numSQL || $this->offset ) {
+ if( $num || $this->offset ) {
$wgOut->addHTML( "<p class='mw-search-pager-bottom'>{$prevnext}</p>\n" );
}
wfProfileOut( __METHOD__ );
}
+ protected function showCreateLink( $t ) {
+ global $wgOut;
+
+ // show direct page/create link if applicable
+ $messageName = null;
+ if( !is_null($t) ) {
+ if( $t->isKnown() ) {
+ $messageName = 'searchmenu-exists';
+ } elseif( $t->userCan( 'create' ) ) {
+ $messageName = 'searchmenu-new';
+ }
+ }
+ if( $messageName ) {
+ $wgOut->addWikiMsg( $messageName, wfEscapeWikiText( $t->getPrefixedText() ) );
+ } else {
+ // preserve the paragraph for margins etc...
+ $wgOut->addHtml( '<p></p>' );
+ }
+ }
+
/**
*
*/
@@ -304,24 +345,25 @@ class SpecialSearch {
$nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
if( $this->searchAdvanced )
$this->active = 'advanced';
- else if( $this->namespaces === NS_FILE || $this->startsWithImage( $term ) )
- $this->active = 'images';
- elseif( $this->namespaces === $nsAllSet )
- $this->active = 'all';
- elseif( $this->namespaces === SearchEngine::defaultNamespaces() )
- $this->active = 'default';
- elseif( $this->namespaces === SearchEngine::projectNamespaces() )
- $this->active = 'project';
- else
- $this->active = 'advanced';
+ else {
+ $profiles = $this->getSearchProfiles();
+
+ foreach( $profiles as $key => $data ) {
+ if ( $this->namespaces == $data['namespaces'] && $key != 'advanced')
+ $this->active = $key;
+ }
+
+ }
# Should advanced UI be used?
$this->searchAdvanced = ($this->active === 'advanced');
if( !empty( $term ) ) {
$wgOut->setPageTitle( wfMsg( 'searchresults') );
$wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term ) ) );
- }
+ }
$wgOut->setArticleRelated( false );
$wgOut->setRobotPolicy( 'noindex,nofollow' );
+ // add javascript specific to special:search
+ $wgOut->addScriptFile( 'search.js' );
}
/**
@@ -358,8 +400,8 @@ class SpecialSearch {
}
/**
- * Show whole set of results
- *
+ * Show whole set of results
+ *
* @param SearchResultSet $matches
*/
protected function showMatches( &$matches ) {
@@ -403,7 +445,20 @@ class SpecialSearch {
$sk = $wgUser->getSkin();
$t = $result->getTitle();
- $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+ $titleSnippet = $result->getTitleSnippet($terms);
+
+ if( $titleSnippet == '' )
+ $titleSnippet = null;
+
+ $link_t = clone $t;
+
+ wfRunHooks( 'ShowSearchHitTitle',
+ array( &$link_t, &$titleSnippet, $result, $terms, $this ) );
+
+ $link = $this->sk->linkKnown(
+ $link_t,
+ $titleSnippet
+ );
//If page content is not readable, just return the title.
//This is not quite safe, but better than showing excerpts from non-readable pages
@@ -427,19 +482,42 @@ class SpecialSearch {
$sectionTitle = $result->getSectionTitle();
$sectionText = $result->getSectionSnippet($terms);
$redirect = '';
- if( !is_null($redirectTitle) )
- $redirect = "<span class='searchalttitle'>"
- .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText))
- ."</span>";
+
+ if( !is_null($redirectTitle) ) {
+ if( $redirectText == '' )
+ $redirectText = null;
+
+ $redirect = "<span class='searchalttitle'>" .
+ wfMsg(
+ 'search-redirect',
+ $this->sk->linkKnown(
+ $redirectTitle,
+ $redirectText
+ )
+ ) .
+ "</span>";
+ }
+
$section = '';
- if( !is_null($sectionTitle) )
- $section = "<span class='searchalttitle'>"
- .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText))
- ."</span>";
+
+
+ if( !is_null($sectionTitle) ) {
+ if( $sectionText == '' )
+ $sectionText = null;
+
+ $section = "<span class='searchalttitle'>" .
+ wfMsg(
+ 'search-section', $this->sk->linkKnown(
+ $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
@@ -454,20 +532,32 @@ class SpecialSearch {
$byteSize = $result->getByteSize();
$wordCount = $result->getWordCount();
$timestamp = $result->getTimestamp();
- $size = wfMsgExt( 'search-result-size', array( 'parsemag', 'escape' ),
- $this->sk->formatSize( $byteSize ), $wordCount );
+ $size = wfMsgExt(
+ 'search-result-size',
+ array( 'parsemag', 'escape' ),
+ $this->sk->formatSize( $byteSize ),
+ $wgLang->formatNum( $wordCount )
+ );
$date = $wgLang->timeanddate( $timestamp );
// link to related articles if supported
$related = '';
if( $result->hasRelated() ) {
$st = SpecialPage::getTitleFor( 'Search' );
- $stParams = wfArrayToCGI( $this->powerSearchOptions(),
- array('search' => wfMsgForContent('searchrelated').':'.$t->getPrefixedText(),
- 'fulltext' => wfMsg('search') ));
-
- $related = ' -- ' . $sk->makeKnownLinkObj( $st,
- wfMsg('search-relatedarticle'), $stParams );
+ $stParams = array_merge(
+ $this->powerSearchOptions(),
+ array(
+ 'search' => wfMsgForContent( 'searchrelated' ) . ':' . $t->getPrefixedText(),
+ 'fulltext' => wfMsg( 'search' )
+ )
+ );
+
+ $related = ' -- ' . $sk->linkKnown(
+ $st,
+ wfMsg('search-relatedarticle'),
+ array(),
+ $stParams
+ );
}
// Include a thumbnail for media files...
@@ -508,7 +598,7 @@ class SpecialSearch {
/**
* Show results from other wikis
- *
+ *
* @param SearchResultSet $matches
*/
protected function showInterwiki( &$matches, $query ) {
@@ -517,7 +607,7 @@ class SpecialSearch {
$terms = $wgContLang->convertForSearchResult( $matches->termMatches() );
$out = "<div id='mw-search-interwiki'><div id='mw-search-interwiki-caption'>".
- wfMsg('search-interwiki-caption')."</div>\n";
+ wfMsg('search-interwiki-caption')."</div>\n";
$off = $this->offset + 1;
$out .= "<ul class='mw-search-iwresults'>\n";
@@ -527,15 +617,15 @@ class SpecialSearch {
foreach($customLines as $line) {
$parts = explode(":",$line,2);
if(count($parts) == 2) // validate line
- $customCaptions[$parts[0]] = $parts[1];
+ $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)..
+ // TODO: 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
@@ -543,63 +633,88 @@ class SpecialSearch {
wfProfileOut( __METHOD__ );
return $out;
}
-
+
/**
* Show single interwiki link
*
* @param SearchResult $result
* @param string $lastInterwiki
* @param array $terms
- * @param string $query
+ * @param string $query
* @param array $customCaptions iw prefix -> caption
*/
protected function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
wfProfileIn( __METHOD__ );
global $wgContLang, $wgLang;
-
+
if( $result->isBrokenTitle() ) {
wfProfileOut( __METHOD__ );
return "<!-- Broken link in search result -->\n";
}
-
+
$t = $result->getTitle();
- $link = $this->sk->makeKnownLinkObj( $t, $result->getTitleSnippet($terms));
+ $titleSnippet = $result->getTitleSnippet($terms);
+
+ if( $titleSnippet == '' )
+ $titleSnippet = null;
+
+ $link = $this->sk->linkKnown(
+ $t,
+ $titleSnippet
+ );
// format redirect if any
$redirectTitle = $result->getRedirectTitle();
$redirectText = $result->getRedirectSnippet($terms);
$redirect = '';
- if( !is_null($redirectTitle) )
- $redirect = "<span class='searchalttitle'>"
- .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText))
- ."</span>";
+ if( !is_null($redirectTitle) ) {
+ if( $redirectText == '' )
+ $redirectText = null;
+
+ $redirect = "<span class='searchalttitle'>" .
+ wfMsg(
+ 'search-redirect',
+ $this->sk->linkKnown(
+ $redirectTitle,
+ $redirectText
+ )
+ ) .
+ "</span>";
+ }
$out = "";
- // display project name
+ // 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
+ // 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']);
- }
+ $caption = wfMsg('search-interwiki-default', $parsed['host']);
+ }
// "more results" link (special page stuff could be localized, but we might not know target lang)
- $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
- $searchLink = $this->sk->makeKnownLinkObj( $searchTitle, wfMsg('search-interwiki-more'),
- wfArrayToCGI(array('search' => $query, 'fulltext' => 'Search')));
+ $searchTitle = Title::newFromText($t->getInterwiki().":Special:Search");
+ $searchLink = $this->sk->linkKnown(
+ $searchTitle,
+ wfMsg('search-interwiki-more'),
+ array(),
+ 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";
+ $out .= "<li>{$link} {$redirect}</li>\n";
wfProfileOut( __METHOD__ );
return $out;
}
-
+
/**
* Generates the power search box at bottom of [[Special:Search]]
@@ -607,172 +722,241 @@ class SpecialSearch {
* @return $out string: HTML form
*/
protected function powerSearchBox( $term ) {
- global $wgScript;
-
- $namespaces = SearchEngine::searchableNamespaces();
-
- $tables = $this->namespaceTables( $namespaces );
-
- $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
- $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
- $searchField = Xml::inputLabel( wfMsg('powersearch-field'), 'search', 'powerSearchText', 50, $term,
- array( 'type' => 'text') );
- $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' )) . "\n";
- $searchTitle = SpecialPage::getTitleFor( 'Search' );
+ global $wgScript, $wgContLang;
- $redirectText = '';
- // show redirects check only if backend supports it
- if( $this->searchEngine->acceptListRedirects() ) {
- $redirectText = "<p>". $redirect . " " . $redirectLabel ."</p>";
+ // Groups namespaces into rows according to subject
+ $rows = array();
+ foreach( SearchEngine::searchableNamespaces() as $namespace => $name ) {
+ $subject = MWNamespace::getSubject( $namespace );
+ if( !array_key_exists( $subject, $rows ) ) {
+ $rows[$subject] = "";
+ }
+ $name = str_replace( '_', ' ', $name );
+ if( $name == '' ) {
+ $name = wfMsg( 'blanknamespace' );
+ }
+ $rows[$subject] .=
+ Xml::openElement(
+ 'td', array( 'style' => 'white-space: nowrap' )
+ ) .
+ Xml::checkLabel(
+ $name,
+ "ns{$namespace}",
+ "mw-search-ns{$namespace}",
+ in_array( $namespace, $this->namespaces )
+ ) .
+ Xml::closeElement( 'td' );
}
+ $rows = array_values( $rows );
+ $numRows = count( $rows );
- $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
- Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n" .
- "<p>" .
- wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
- "</p>\n" .
- '<input type="hidden" name="advanced" value="'.$this->searchAdvanced."\"/>\n".
- $tables .
- "<hr style=\"clear: both;\" />\n".
- $redirectText ."\n".
- "<div style=\"padding-top:2px;padding-bottom:2px;\">".
- $searchField .
- "&nbsp;" .
- Xml::hidden( 'fulltext', 'Advanced search' ) . "\n" .
- $searchButton .
- "</div>".
- "</form>";
- $t = Title::newFromText( $term );
- /* if( $t != null && count($this->namespaces) === 1 ) {
- $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term );
- } */
- return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) .
+ // Lays out namespaces in multiple floating two-column tables so they'll
+ // be arranged nicely while still accommodating different screen widths
+ $namespaceTables = '';
+ for( $i = 0; $i < $numRows; $i += 4 ) {
+ $namespaceTables .= Xml::openElement(
+ 'table',
+ array( 'cellpadding' => 0, 'cellspacing' => 0, 'border' => 0 )
+ );
+ for( $j = $i; $j < $i + 4 && $j < $numRows; $j++ ) {
+ $namespaceTables .= Xml::tags( 'tr', null, $rows[$j] );
+ }
+ $namespaceTables .= Xml::closeElement( 'table' );
+ }
+ // Show redirects check only if backend supports it
+ $redirects = '';
+ if( $this->searchEngine->acceptListRedirects() ) {
+ $redirects =
+ Xml::check(
+ 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' )
+ ) .
+ ' ' .
+ Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
+ }
+ // Return final output
+ return
+ Xml::openElement(
+ 'fieldset',
+ array( 'id' => 'mw-searchoptions', 'style' => 'margin:0em;' )
+ ) .
Xml::element( 'legend', null, wfMsg('powersearch-legend') ) .
- $this->formHeader($term) . $out . $this->didYouMeanHtml .
+ Xml::tags( 'h4', null, wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) ) .
+ Xml::tags(
+ 'div',
+ array( 'id' => 'mw-search-togglebox' ),
+ Xml::label( wfMsg( 'powersearch-togglelabel' ), 'mw-search-togglelabel' ) .
+ Xml::element(
+ 'input',
+ array(
+ 'type'=>'button',
+ 'id' => 'mw-search-toggleall',
+ 'onclick' => 'mwToggleSearchCheckboxes("all");',
+ 'value' => wfMsg( 'powersearch-toggleall' )
+ )
+ ) .
+ Xml::element(
+ 'input',
+ array(
+ 'type'=>'button',
+ 'id' => 'mw-search-togglenone',
+ 'onclick' => 'mwToggleSearchCheckboxes("none");',
+ 'value' => wfMsg( 'powersearch-togglenone' )
+ )
+ )
+ ) .
+ Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
+ $namespaceTables .
+ Xml::element( 'div', array( 'class' => 'divider' ), '', false ) .
+ $redirects .
+ Xml::hidden( 'title', SpecialPage::getTitleFor( 'Search' )->getPrefixedText() ) .
+ Xml::hidden( 'advanced', $this->searchAdvanced ) .
+ Xml::hidden( 'fulltext', 'Advanced search' ) .
Xml::closeElement( 'fieldset' );
}
-
+
protected function searchFocus() {
- global $wgJsMimeType;
- return "<script type=\"$wgJsMimeType\">" .
+ $id = $this->searchAdvanced ? 'powerSearchText' : 'searchText';
+ return Html::inlineScript(
"hookEvent(\"load\", function() {" .
- "document.getElementById('searchText').focus();" .
- "});" .
- "</script>";
+ "document.getElementById('$id').focus();" .
+ "});" );
}
+
+ protected function getSearchProfiles() {
+ // Builds list of Search Types (profiles)
+ $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
+
+ $profiles = array(
+ 'default' => array(
+ 'message' => 'searchprofile-articles',
+ 'tooltip' => 'searchprofile-articles-tooltip',
+ 'namespaces' => SearchEngine::defaultNamespaces(),
+ 'namespace-messages' => SearchEngine::namespacesAsText(
+ SearchEngine::defaultNamespaces()
+ ),
+ ),
+ 'images' => array(
+ 'message' => 'searchprofile-images',
+ 'tooltip' => 'searchprofile-images-tooltip',
+ 'namespaces' => array( NS_FILE ),
+ ),
+ 'help' => array(
+ 'message' => 'searchprofile-project',
+ 'tooltip' => 'searchprofile-project-tooltip',
+ 'namespaces' => SearchEngine::helpNamespaces(),
+ 'namespace-messages' => SearchEngine::namespacesAsText(
+ SearchEngine::helpNamespaces()
+ ),
+ ),
+ 'all' => array(
+ 'message' => 'searchprofile-everything',
+ 'tooltip' => 'searchprofile-everything-tooltip',
+ 'namespaces' => $nsAllSet,
+ ),
+ 'advanced' => array(
+ 'message' => 'searchprofile-advanced',
+ 'tooltip' => 'searchprofile-advanced-tooltip',
+ 'namespaces' => $this->namespaces,
+ 'parameters' => array( 'advanced' => 1 ),
+ )
+ );
+
+ wfRunHooks( 'SpecialSearchProfiles', array( &$profiles ) );
- protected function powerSearchFocus() {
- global $wgJsMimeType;
- return "<script type=\"$wgJsMimeType\">" .
- "hookEvent(\"load\", function() {" .
- "document.getElementById('powerSearchText').focus();" .
- "});" .
- "</script>";
+ foreach( $profiles as $key => &$data ) {
+ sort($data['namespaces']);
+ }
+
+ return $profiles;
}
- protected function formHeader( $term ) {
- global $wgContLang, $wgCanonicalNamespaceNames, $wgLang;
-
- $sep = '&nbsp;&nbsp;&nbsp;';
- $out = Xml::openElement('div', array( 'style' => 'padding-bottom:0.5em;' ) );
-
+ protected function formHeader( $term, $resultsShown, $totalNum ) {
+ global $wgContLang, $wgLang;
+
+ $out = Xml::openElement('div', array( 'class' => 'mw-search-formheader' ) );
+
$bareterm = $term;
- if( $this->startsWithImage( $term ) )
- $bareterm = substr( $term, strpos( $term, ':' ) + 1 ); // delete all/image prefix
-
- $nsAllSet = array_keys( SearchEngine::searchableNamespaces() );
-
- // search profiles headers
- $m = wfMsg( 'searchprofile-articles' );
- $tt = wfMsg( 'searchprofile-articles-tooltip',
- $wgLang->commaList( SearchEngine::namespacesAsText( SearchEngine::defaultNamespaces() ) ) );
- if( $this->active == 'default' ) {
- $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
- } else {
- $out .= $this->makeSearchLink( $bareterm, SearchEngine::defaultNamespaces(), $m, $tt );
+ if( $this->startsWithImage( $term ) ) {
+ // Deletes prefixes
+ $bareterm = substr( $term, strpos( $term, ':' ) + 1 );
}
- $out .= $sep;
+
- $m = wfMsg( 'searchprofile-images' );
- $tt = wfMsg( 'searchprofile-images-tooltip' );
- if( $this->active == 'images' ) {
- $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
- } else {
- $imageTextForm = $wgContLang->getFormattedNsText(NS_FILE).':'.$bareterm;
- $out .= $this->makeSearchLink( $imageTextForm, array( NS_FILE ) , $m, $tt );
+ $profiles = $this->getSearchProfiles();
+
+ // Outputs XML for Search Types
+ $out .= Xml::openElement( 'div', array( 'class' => 'search-types' ) );
+ $out .= Xml::openElement( 'ul' );
+ foreach ( $profiles as $id => $profile ) {
+ $tooltipParam = isset( $profile['namespace-messages'] ) ?
+ $wgLang->commaList( $profile['namespace-messages'] ) : null;
+ $out .= Xml::tags(
+ 'li',
+ array(
+ 'class' => $this->active == $id ? 'current' : 'normal'
+ ),
+ $this->makeSearchLink(
+ $bareterm,
+ $profile['namespaces'],
+ wfMsg( $profile['message'] ),
+ wfMsg( $profile['tooltip'], $tooltipParam ),
+ isset( $profile['parameters'] ) ? $profile['parameters'] : array()
+ )
+ );
}
- $out .= $sep;
+ $out .= Xml::closeElement( 'ul' );
+ $out .= Xml::closeElement('div') ;
- $m = wfMsg( 'searchprofile-project' );
- $tt = wfMsg( 'searchprofile-project-tooltip',
- $wgLang->commaList( SearchEngine::namespacesAsText( SearchEngine::projectNamespaces() ) ) );
- if( $this->active == 'project' ) {
- $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
- } else {
- $out .= $this->makeSearchLink( $bareterm, SearchEngine::projectNamespaces(), $m, $tt );
- }
- $out .= $sep;
-
- $m = wfMsg( 'searchprofile-everything' );
- $tt = wfMsg( 'searchprofile-everything-tooltip' );
- if( $this->active == 'all' ) {
- $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
- } else {
- $out .= $this->makeSearchLink( $bareterm, $nsAllSet, $m, $tt );
+ // Results-info
+ if ( $resultsShown > 0 ) {
+ if ( $totalNum > 0 ){
+ $top = wfMsgExt( 'showingresultsheader', array( 'parseinline' ),
+ $wgLang->formatNum( $this->offset + 1 ),
+ $wgLang->formatNum( $this->offset + $resultsShown ),
+ $wgLang->formatNum( $totalNum ),
+ wfEscapeWikiText( $term ),
+ $wgLang->formatNum( $resultsShown )
+ );
+ } elseif ( $resultsShown >= $this->limit ) {
+ $top = wfShowingResults( $this->offset, $this->limit );
+ } else {
+ $top = wfShowingResultsNum( $this->offset, $this->limit, $resultsShown );
+ }
+ $out .= Xml::tags( 'div', array( 'class' => 'results-info' ),
+ Xml::tags( 'ul', null, Xml::tags( 'li', null, $top ) )
+ );
}
- $out .= $sep;
- $m = wfMsg( 'searchprofile-advanced' );
- $tt = wfMsg( 'searchprofile-advanced-tooltip' );
- if( $this->active == 'advanced' ) {
- $out .= Xml::element( 'strong', array( 'title'=>$tt ), $m );
- } else {
- $out .= $this->makeSearchLink( $bareterm, $this->namespaces, $m, $tt, array( 'advanced' => '1' ) );
+ $out .= Xml::element( 'div', array( 'style' => 'clear:both' ), '', false );
+ $out .= Xml::closeElement('div');
+
+ // Adds hidden namespace fields
+ if ( !$this->searchAdvanced ) {
+ foreach( $this->namespaces as $ns ) {
+ $out .= Xml::hidden( "ns{$ns}", '1' );
+ }
}
- $out .= Xml::closeElement('div') ;
return $out;
}
-
+
protected function shortDialog( $term ) {
- global $wgScript;
$searchTitle = SpecialPage::getTitleFor( 'Search' );
$searchable = SearchEngine::searchableNamespaces();
- $out = Xml::openElement( 'form', array( 'id' => 'search', 'method' => 'get', 'action' => $wgScript ) );
- $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
- // show namespaces only for advanced search
- if( $this->active == 'advanced' ) {
- $active = array();
- foreach( $this->namespaces as $ns ) {
- $active[$ns] = $searchable[$ns];
- }
- $out .= wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . "<br/>\n";
- $out .= $this->namespaceTables( $active, 1 )."<br/>\n";
- // Still keep namespace settings otherwise, but don't show them
- } else {
- foreach( $this->namespaces as $ns ) {
- $out .= Xml::hidden( "ns{$ns}", '1' );
- }
- }
+ $out = Html::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
// Keep redirect setting
- $out .= Xml::hidden( "redirs", (int)$this->searchRedirects );
+ $out .= Html::hidden( "redirs", (int)$this->searchRedirects ) . "\n";
// Term box
- $out .= Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'searchText' ) ) . "\n";
- $out .= Xml::hidden( 'fulltext', 'Search' );
- $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
- $out .= ' (' . wfMsgExt('searchmenu-help',array('parseinline') ) . ')';
- $out .= Xml::closeElement( 'form' );
- // Add prefix link for single-namespace searches
- $t = Title::newFromText( $term );
- /*if( $t != null && count($this->namespaces) === 1 ) {
- $out .= wfMsgExt( 'searchmenu-prefix', array('parseinline'), $term );
- }*/
- return Xml::openElement( 'fieldset', array('id' => 'mw-searchoptions','style' => 'margin:0em;') ) .
- Xml::element( 'legend', null, wfMsg('searchmenu-legend') ) .
- $this->formHeader($term) . $out . $this->didYouMeanHtml .
- Xml::closeElement( 'fieldset' );
+ $out .= Html::input( 'search', $term, 'search', array(
+ 'id' => $this->searchAdvanced ? 'powerSearchText' : 'searchText',
+ 'size' => '50',
+ 'autofocus'
+ ) ) . "\n";
+ $out .= Html::hidden( 'fulltext', 'Search' ) . "\n";
+ $out .= Xml::submitButton( wfMsg( 'searchbutton' ) ) . "\n";
+ return $out . $this->didYouMeanHtml;
}
-
+
/** Make a search link with some target namespaces */
protected function makeSearchLink( $term, $namespaces, $label, $tooltip, $params=array() ) {
$opt = $params;
@@ -781,18 +965,30 @@ class SpecialSearch {
}
$opt['redirs'] = $this->searchRedirects ? 1 : 0;
- $st = SpecialPage::getTitleFor( 'Search' );
- $stParams = wfArrayToCGI( array( 'search' => $term, 'fulltext' => wfMsg( 'search' ) ), $opt );
+ $st = SpecialPage::getTitleFor( 'Search' );
+ $stParams = array_merge(
+ array(
+ 'search' => $term,
+ 'fulltext' => wfMsg( 'search' )
+ ),
+ $opt
+ );
- return Xml::element( 'a',
- array( 'href'=> $st->getLocalURL( $stParams ), 'title' => $tooltip ),
- $label );
+ return Xml::element(
+ 'a',
+ array(
+ 'href' => $st->getLocalURL( $stParams ),
+ 'title' => $tooltip,
+ 'onmousedown' => 'mwSearchHeaderClick(this);',
+ 'onkeydown' => 'mwSearchHeaderClick(this);'),
+ $label
+ );
}
-
+
/** Check if query starts with image: prefix */
protected function startsWithImage( $term ) {
global $wgContLang;
-
+
$p = explode( ':', $term );
if( count( $p ) > 1 ) {
return $wgContLang->getNsIndex( $p[0] ) == NS_FILE;
@@ -800,689 +996,16 @@ class SpecialSearch {
return false;
}
- protected function namespaceTables( $namespaces, $rowsPerTable = 3 ) {
- global $wgContLang;
- // Group namespaces into rows according to subject.
- // Try not to make too many assumptions about namespace numbering.
- $rows = array();
- $tables = "";
- foreach( $namespaces as $ns => $name ) {
- $subj = MWNamespace::getSubject( $ns );
- if( !array_key_exists( $subj, $rows ) ) {
- $rows[$subj] = "";
- }
- $name = str_replace( '_', ' ', $name );
- if( '' == $name ) {
- $name = wfMsg( 'blanknamespace' );
- }
- $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) .
- Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
- Xml::closeElement( 'td' ) . "\n";
- }
- $rows = array_values( $rows );
- $numRows = count( $rows );
- // Lay out namespaces in multiple floating two-column tables so they'll
- // be arranged nicely while still accommodating different screen widths
- // Float to the right on RTL wikis
- $tableStyle = $wgContLang->isRTL() ?
- 'float: right; margin: 0 0 0em 1em' : 'float: left; margin: 0 1em 0em 0';
- // Build the final HTML table...
- for( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
- $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) );
- for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
- $tables .= "<tr>\n" . $rows[$j] . "</tr>";
- }
- $tables .= Xml::closeElement( 'table' ) . "\n";
- }
- return $tables;
- }
-}
-
-/**
- * implements Special:Search - Run text & title search and display the output
- * @ingroup SpecialPage
- */
-class SpecialSearchOld {
-
- /**
- * Set up basic search parameters from the request and user settings.
- * Typically you'll pass $wgRequest and $wgUser.
- *
- * @param WebRequest $request
- * @param User $user
- * @public
- */
- function __construct( &$request, &$user ) {
- list( $this->limit, $this->offset ) = $request->getLimitOffset( 20, 'searchlimit' );
- $this->mPrefix = $request->getVal('prefix', '');
- $this->namespaces = $this->powerSearch( $request );
- if( empty( $this->namespaces ) ) {
- $this->namespaces = SearchEngine::userNamespaces( $user );
- }
-
- $this->searchRedirects = $request->getcheck( 'redirs' ) ? true : false;
- $this->fulltext = $request->getVal('fulltext');
- }
-
- /**
- * 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;
- }
- }
-
- $extra = $wgOut->parse( '=='.wfMsgNoTrans( 'notitlematches' )."==\n" );
- if( $t->quickUserCan( 'create' ) && $t->quickUserCan( 'edit' ) ) {
- $extra .= wfMsgExt( 'noexactmatch', 'parse', wfEscapeWikiText( $term ) );
- } else {
- $extra .= wfMsgExt( 'noexactmatch-nocreate', 'parse', wfEscapeWikiText( $term ) );
- }
-
- $this->showResults( $term, $extra );
- }
-
- /**
- * @param string $term
- * @param string $extra Extra HTML to add after "did you mean"
- */
- public function showResults( $term, $extra = '' ) {
- wfProfileIn( __METHOD__ );
- global $wgOut, $wgUser;
- $sk = $wgUser->getSkin();
-
- $search = SearchEngine::create();
- $search->setLimitOffset( $this->limit, $this->offset );
- $search->setNamespaces( $this->namespaces );
- $search->showRedirects = $this->searchRedirects;
- $search->prefix = $this->mPrefix;
- $term = $search->transformSearchTerm($term);
-
- $this->setupPage( $term );
-
- $rewritten = $search->replacePrefixes($term);
- $titleMatches = $search->searchTitle( $rewritten );
- $textMatches = $search->searchText( $rewritten );
-
- // did you mean... suggestions
- if($textMatches && $textMatches->hasSuggestion()){
- $st = SpecialPage::getTitleFor( 'Search' );
-
- # mirror Go/Search behaviour of original request
- $didYouMeanParams = array( 'search' => $textMatches->getSuggestionQuery() );
- if($this->fulltext != NULL)
- $didYouMeanParams['fulltext'] = $this->fulltext;
- $stParams = wfArrayToCGI(
- $didYouMeanParams,
- $this->powerSearchOptions()
- );
-
- $suggestLink = $sk->makeKnownLinkObj( $st,
- $textMatches->getSuggestionSnippet(),
- $stParams );
-
- $wgOut->addHTML('<div class="searchdidyoumean">'.wfMsg('search-suggest',$suggestLink).'</div>');
- }
-
- $wgOut->addHTML( $extra );
-
- $wgOut->wrapWikiMsg( "<div class='mw-searchresult'>\n$1</div>", 'searchresulttext' );
-
- if( '' === trim( $term ) ) {
- // Empty query -- straight view of search form
- $wgOut->setSubtitle( '' );
- $wgOut->addHTML( $this->powerSearchBox( $term ) );
- $wgOut->addHTML( $this->powerSearchFocus() );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- global $wgDisableTextSearch;
- if ( $wgDisableTextSearch ) {
- global $wgSearchForwardUrl;
- if( $wgSearchForwardUrl ) {
- $url = str_replace( '$1', urlencode( $term ), $wgSearchForwardUrl );
- $wgOut->redirect( $url );
- wfProfileOut( __METHOD__ );
- return;
- }
- global $wgInputEncoding;
- $wgOut->addHTML(
- Xml::openElement( 'fieldset' ) .
- Xml::element( 'legend', null, wfMsg( 'search-external' ) ) .
- Xml::element( 'p', array( 'class' => 'mw-searchdisabled' ), wfMsg( 'searchdisabled' ) ) .
- wfMsg( 'googlesearch',
- htmlspecialchars( $term ),
- htmlspecialchars( $wgInputEncoding ),
- htmlspecialchars( wfMsg( 'searchbutton' ) )
- ) .
- Xml::closeElement( 'fieldset' )
- );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- $wgOut->addHTML( $this->shortDialog( $term ) );
-
- // Sometimes the search engine knows there are too many hits
- if ($titleMatches instanceof SearchResultTooMany) {
- $wgOut->addWikiText( '==' . wfMsg( 'toomanymatches' ) . "==\n" );
- $wgOut->addHTML( $this->powerSearchBox( $term ) );
- $wgOut->addHTML( $this->powerSearchFocus() );
- wfProfileOut( __METHOD__ );
- return;
- }
-
- // 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, $num );
- } 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( __METHOD__ );
- }
-
- #------------------------------------------------------------------
- # Private methods below this line
-
- /**
- *
- */
- function setupPage( $term ) {
- global $wgOut;
- if( !empty( $term ) ){
- $wgOut->setPageTitle( wfMsg( 'searchresults') );
- $wgOut->setHTMLTitle( wfMsg( 'pagetitle', wfMsg( 'searchresults-title', $term) ) );
- }
- $subtitlemsg = ( Title::newFromText( $term ) ? 'searchsubtitle' : 'searchsubtitleinvalid' );
- $wgOut->setSubtitle( $wgOut->parse( wfMsg( $subtitlemsg, wfEscapeWikiText($term) ) ) );
- $wgOut->setArticleRelated( false );
- $wgOut->setRobotPolicy( 'noindex,nofollow' );
- }
-
- /**
- * 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 ) {
- wfProfileIn( __METHOD__ );
-
- 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( __METHOD__ );
- return $out;
- }
+ /** Check if query starts with all: prefix */
+ protected function startsWithAll( $term ) {
- /**
- * Format a single hit result
- * @param SearchResult $result
- * @param array $terms terms to highlight
- */
- function showHit( $result, $terms ) {
- wfProfileIn( __METHOD__ );
- global $wgUser, $wgContLang, $wgLang;
+ $allkeyword = wfMsgForContent('searchall');
- if( $result->isBrokenTitle() ) {
- wfProfileOut( __METHOD__ );
- 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( __METHOD__ );
- return "<li>{$link}</li>\n";
- }
-
- // If the page doesn't *exist*... our search index is out of date.
- // The least confusing at this point is to drop the result.
- // You may get less results, but... oh well. :P
- if( $result->isMissingRevision() ) {
- wfProfileOut( __METHOD__ );
- return "<!-- missing page " .
- htmlspecialchars( $t->getPrefixedText() ) . "-->\n";
- }
-
- // format redirects / relevant sections
- $redirectTitle = $result->getRedirectTitle();
- $redirectText = $result->getRedirectSnippet($terms);
- $sectionTitle = $result->getSectionTitle();
- $sectionText = $result->getSectionSnippet($terms);
- $redirect = '';
- if( !is_null($redirectTitle) )
- $redirect = "<span class='searchalttitle'>"
- .wfMsg('search-redirect',$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 = ' -- ' . $sk->makeKnownLinkObj( $st,
- wfMsg('search-relatedarticle'), $stParams );
- }
-
- // Include a thumbnail for media files...
- if( $t->getNamespace() == NS_FILE ) {
- $img = wfFindFile( $t );
- if( $img ) {
- $thumb = $img->transform( array( 'width' => 120, 'height' => 120 ) );
- if( $thumb ) {
- $desc = $img->getShortDesc();
- wfProfileOut( __METHOD__ );
- // 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( __METHOD__ );
- return "<li>{$link} {$redirect} {$section} {$extract}\n" .
- "<div class='mw-search-result-data'>{$score}{$size} - {$date}{$related}</div>" .
- "</li>\n";
-
- }
-
- /**
- * Show results from other wikis
- *
- * @param SearchResultSet $matches
- */
- function showInterwiki( &$matches, $query ) {
- wfProfileIn( __METHOD__ );
-
- 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( __METHOD__ );
- return $out;
- }
-
- /**
- * Show single interwiki link
- *
- * @param SearchResult $result
- * @param string $lastInterwiki
- * @param array $terms
- * @param string $query
- * @param array $customCaptions iw prefix -> caption
- */
- function showInterwikiHit( $result, $lastInterwiki, $terms, $query, $customCaptions) {
- wfProfileIn( __METHOD__ );
- global $wgUser, $wgContLang, $wgLang;
-
- if( $result->isBrokenTitle() ) {
- wfProfileOut( __METHOD__ );
- 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( __METHOD__ );
- 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, $wgContLang;
-
- $namespaces = SearchEngine::searchableNamespaces();
-
- // group namespaces into rows according to subject; try not to make too
- // many assumptions about namespace numbering
- $rows = array();
- foreach( $namespaces as $ns => $name ) {
- $subj = MWNamespace::getSubject( $ns );
- if( !array_key_exists( $subj, $rows ) ) {
- $rows[$subj] = "";
- }
- $name = str_replace( '_', ' ', $name );
- if( '' == $name ) {
- $name = wfMsg( 'blanknamespace' );
- }
- $rows[$subj] .= Xml::openElement( 'td', array( 'style' => 'white-space: nowrap' ) ) .
- Xml::checkLabel( $name, "ns{$ns}", "mw-search-ns{$ns}", in_array( $ns, $this->namespaces ) ) .
- Xml::closeElement( 'td' ) . "\n";
- }
- $rows = array_values( $rows );
- $numRows = count( $rows );
-
- // lay out namespaces in multiple floating two-column tables so they'll
- // be arranged nicely while still accommodating different screen widths
- $rowsPerTable = 3; // seems to look nice
-
- // float to the right on RTL wikis
- $tableStyle = ( $wgContLang->isRTL() ?
- 'float: right; margin: 0 0 1em 1em' :
- 'float: left; margin: 0 1em 1em 0' );
-
- $tables = "";
- for( $i = 0; $i < $numRows; $i += $rowsPerTable ) {
- $tables .= Xml::openElement( 'table', array( 'style' => $tableStyle ) );
- for( $j = $i; $j < $i + $rowsPerTable && $j < $numRows; $j++ ) {
- $tables .= "<tr>\n" . $rows[$j] . "</tr>";
- }
- $tables .= Xml::closeElement( 'table' ) . "\n";
- }
-
- $redirect = Xml::check( 'redirs', $this->searchRedirects, array( 'value' => '1', 'id' => 'redirs' ) );
- $redirectLabel = Xml::label( wfMsg( 'powersearch-redir' ), 'redirs' );
- $searchField = Xml::input( 'search', 50, $term, array( 'type' => 'text', 'id' => 'powerSearchText' ) );
- $searchButton = Xml::submitButton( wfMsg( 'powersearch' ), array( 'name' => 'fulltext' ) ) . "\n";
- $searchTitle = SpecialPage::getTitleFor( 'Search' );
- $searchHiddens = Xml::hidden( 'title', $searchTitle->getPrefixedText() ) . "\n";
- $searchHiddens .= Xml::hidden( 'fulltext', 'Advanced search' ) . "\n";
-
- $out = Xml::openElement( 'form', array( 'id' => 'powersearch', 'method' => 'get', 'action' => $wgScript ) ) .
- Xml::fieldset( wfMsg( 'powersearch-legend' ),
- "<p>" .
- wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) .
- "</p>\n" .
- $tables .
- "<hr style=\"clear: both\" />\n" .
- "<p>" .
- $redirect . " " . $redirectLabel .
- "</p>\n" .
- wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) .
- "&nbsp;" .
- $searchField .
- "&nbsp;" .
- $searchHiddens .
- $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
- ));
- $searchTitle = SpecialPage::getTitleFor( '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' );
- }
+ $p = explode( ':', $term );
+ if( count( $p ) > 1 ) {
+ return $p[0] == $allkeyword;
}
- $out .= Xml::hidden( 'title', $searchTitle->getPrefixedText() );
- $out .= Xml::hidden( 'fulltext', 'Search' );
- $out .= Xml::submitButton( wfMsg( 'searchbutton' ), array( 'name' => 'fulltext' ) );
- $out .= Xml::closeElement( 'form' );
-
- return $out;
+ return false;
}
}
+
diff --git a/includes/specials/SpecialShortpages.php b/includes/specials/SpecialShortpages.php
index 2e7d24a5..c41b15c5 100644
--- a/includes/specials/SpecialShortpages.php
+++ b/includes/specials/SpecialShortpages.php
@@ -74,10 +74,15 @@ class ShortPagesPage extends QueryPage {
if ( !$title ) {
return '<!-- Invalid title ' . htmlspecialchars( "{$result->namespace}:{$result->title}" ). '-->';
}
- $hlink = $skin->makeKnownLinkObj( $title, wfMsgHtml( 'hist' ), 'action=history' );
+ $hlink = $skin->linkKnown(
+ $title,
+ wfMsgHtml( 'hist' ),
+ array(),
+ array( 'action' => 'history' )
+ );
$plink = $this->isCached()
- ? $skin->makeLinkObj( $title )
- : $skin->makeKnownLinkObj( $title );
+ ? $skin->link( $title )
+ : $skin->linkKnown( $title );
$size = wfMsgExt( 'nbytes', array( 'parsemag', 'escape' ), $wgLang->formatNum( htmlspecialchars( $result->value ) ) );
return $title->exists()
diff --git a/includes/specials/SpecialSpecialpages.php b/includes/specials/SpecialSpecialpages.php
index 4959f107..84ab689a 100644
--- a/includes/specials/SpecialSpecialpages.php
+++ b/includes/specials/SpecialSpecialpages.php
@@ -55,15 +55,15 @@ function wfSpecialSpecialpages() {
$total = count($sortedPages);
$count = 0;
- $wgOut->wrapWikiMsg( "<h4 class='mw-specialpagesgroup'>$1</h4>\n", "specialpages-group-$group" );
+ $wgOut->wrapWikiMsg( "<h4 class=\"mw-specialpagesgroup\" id=\"mw-specialpagesgroup-$group\">$1</h4>\n", "specialpages-group-$group" );
$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 ) );
+ $link = $sk->linkKnown( $title , htmlspecialchars( $desc ) );
if( $restricted ) {
$includesRestrictedPages = true;
- $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'>{$link}</li>\n" );
+ $wgOut->addHTML( "<li class='mw-specialpages-page mw-specialpagerestricted'><strong>{$link}</strong></li>\n" );
} else {
$wgOut->addHTML( "<li>{$link}</li>\n" );
}
diff --git a/includes/specials/SpecialStatistics.php b/includes/specials/SpecialStatistics.php
index 109c5c30..2e785b8b 100644
--- a/includes/specials/SpecialStatistics.php
+++ b/includes/specials/SpecialStatistics.php
@@ -23,7 +23,7 @@ class SpecialStatistics extends SpecialPage {
}
public function execute( $par ) {
- global $wgOut, $wgRequest, $wgMessageCache;
+ global $wgOut, $wgRequest, $wgMessageCache, $wgMemc;
global $wgDisableCounters, $wgMiserMode;
$wgMessageCache->loadAllMessages();
@@ -38,6 +38,7 @@ class SpecialStatistics extends SpecialPage {
$this->activeUsers = SiteStats::activeUsers();
$this->admins = SiteStats::numberingroup('sysop');
$this->numJobs = SiteStats::jobs();
+ $this->hook = '';
# Staticic - views
$viewsStats = '';
@@ -47,8 +48,13 @@ class SpecialStatistics extends SpecialPage {
# Set active user count
if( !$wgMiserMode ) {
- $dbw = wfGetDB( DB_MASTER );
- SiteStatsUpdate::cacheUpdate( $dbw );
+ $key = wfMemcKey( 'sitestats', 'activeusers-updated' );
+ // Re-calculate the count if the last tally is old...
+ if( !$wgMemc->get($key) ) {
+ $dbw = wfGetDB( DB_MASTER );
+ SiteStatsUpdate::cacheUpdate( $dbw );
+ $wgMemc->set( $key, '1', 24*3600 ); // don't update for 1 day
+ }
}
# Do raw output
@@ -56,10 +62,10 @@ class SpecialStatistics extends SpecialPage {
$this->doRawOutput();
}
- $text = Xml::openElement( 'table', array( 'class' => 'mw-statistics-table' ) );
+ $text = Xml::openElement( 'table', array( 'class' => 'wikitable mw-statistics-table' ) );
# Statistic - pages
- $text .= $this->getPageStats();
+ $text .= $this->getPageStats();
# Statistic - edits
$text .= $this->getEditStats();
@@ -75,6 +81,12 @@ class SpecialStatistics extends SpecialPage {
if( !$wgDisableCounters && !$wgMiserMode ) {
$text .= $this->getMostViewedPages();
}
+
+ # Statistic - other
+ $extraStats = array();
+ if( wfRunHooks( 'SpecialStatsAddExtra', array( &$extraStats ) ) ) {
+ $text .= $this->getOtherStats( $extraStats );
+ }
$text .= Xml::closeElement( 'table' );
@@ -149,14 +161,22 @@ class SpecialStatistics extends SpecialPage {
array( 'class' => 'mw-statistics-jobqueue' ) );
}
private function getUserStats() {
- global $wgLang, $wgRCMaxAge;
+ global $wgLang, $wgUser, $wgRCMaxAge;
+ $sk = $wgUser->getSkin();
return Xml::openElement( 'tr' ) .
Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-users', array( 'parseinline' ) ) ) .
Xml::closeElement( 'tr' ) .
$this->formatRow( wfMsgExt( 'statistics-users', array( 'parseinline' ) ),
$wgLang->formatNum( $this->users ),
array( 'class' => 'mw-statistics-users' ) ) .
- $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ),
+ $this->formatRow( wfMsgExt( 'statistics-users-active', array( 'parseinline' ) ) . ' ' .
+ $sk->link(
+ SpecialPage::getTitleFor( 'Activeusers' ),
+ wfMsgHtml( 'listgrouprights-members' ),
+ array(),
+ array(),
+ 'known'
+ ),
$wgLang->formatNum( $this->activeUsers ),
array( 'class' => 'mw-statistics-users-active' ),
'statistics-users-active-desc',
@@ -184,13 +204,19 @@ class SpecialStatistics extends SpecialPage {
} else {
$grouppageLocalized = $msg;
}
- $grouppage = $sk->makeLink( $grouppageLocalized, htmlspecialchars( $groupnameLocalized ) );
- $grouplink = $sk->link( SpecialPage::getTitleFor( 'Listusers' ),
+ $linkTarget = Title::newFromText( $grouppageLocalized );
+ $grouppage = $sk->link(
+ $linkTarget,
+ htmlspecialchars( $groupnameLocalized )
+ );
+ $grouplink = $sk->link(
+ SpecialPage::getTitleFor( 'Listusers' ),
wfMsgHtml( 'listgrouprights-members' ),
array(),
array( 'group' => $group ),
- 'known' );
- # Add a class when a usergroup contains no members to allow hiding these rows
+ 'known'
+ );
+ # Add a class when a usergroup contains no members to allow hiding these rows
$classZero = '';
$countUsers = SiteStats::numberingroup( $groupname );
if( $countUsers == 0 ) {
@@ -238,7 +264,9 @@ class SpecialStatistics extends SpecialPage {
)
);
if( $res->numRows() > 0 ) {
+ $text .= Xml::openElement( 'tr' );
$text .= Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-mostpopular', array( 'parseinline' ) ) );
+ $text .= Xml::closeElement( 'tr' );
while( $row = $res->fetchObject() ) {
$title = Title::makeTitleSafe( $row->page_namespace, $row->page_title );
if( $title instanceof Title ) {
@@ -252,6 +280,26 @@ class SpecialStatistics extends SpecialPage {
return $text;
}
+ private function getOtherStats( $stats ) {
+ global $wgLang;
+
+ if ( !count( $stats ) )
+ return '';
+
+ $return = Xml::openElement( 'tr' ) .
+ Xml::tags( 'th', array( 'colspan' => '2' ), wfMsgExt( 'statistics-header-hooks', array( 'parseinline' ) ) ) .
+ Xml::closeElement( 'tr' );
+
+ foreach( $stats as $name => $number ) {
+ $name = htmlspecialchars( $name );
+ $number = htmlspecialchars( $number );
+
+ $return .= $this->formatRow( $name, $wgLang->formatNum( $number ), array( 'class' => 'mw-statistics-hook' ) );
+ }
+
+ return $return;
+ }
+
/**
* Do the action=raw output for this page. Legacy, but we support
* it for backwards compatibility
diff --git a/includes/specials/SpecialTags.php b/includes/specials/SpecialTags.php
index 981eb2ff..57feeae7 100644
--- a/includes/specials/SpecialTags.php
+++ b/includes/specials/SpecialTags.php
@@ -36,7 +36,7 @@ class SpecialTags extends SpecialPage {
$html .= $this->doTagRow( $tag, 0 );
}
- $wgOut->addHTML( Xml::tags( 'table', array( 'class' => 'mw-tags-table' ), $html ) );
+ $wgOut->addHTML( Xml::tags( 'table', array( 'class' => 'wikitable mw-tags-table' ), $html ) );
}
function doTagRow( $tag, $hitcount ) {
@@ -49,21 +49,23 @@ class SpecialTags extends SpecialPage {
if ( in_array( $tag, $doneTags ) ) {
return '';
}
+
+ global $wgLang;
$newRow = '';
$newRow .= Xml::tags( 'td', null, Xml::element( 'tt', null, $tag ) );
$disp = ChangeTags::tagDescription( $tag );
- $disp .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsg( 'tags-edit' ) ) . ')';
+ $disp .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag" ), wfMsgHtml( 'tags-edit' ) ) . ')';
$newRow .= Xml::tags( 'td', null, $disp );
$desc = wfMsgExt( "tag-$tag-description", 'parseinline' );
$desc = wfEmptyMsg( "tag-$tag-description", $desc ) ? '' : $desc;
- $desc .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsg( 'tags-edit' ) ) . ')';
+ $desc .= ' (' . $sk->link( Title::makeTitle( NS_MEDIAWIKI, "Tag-$tag-description" ), wfMsgHtml( 'tags-edit' ) ) . ')';
$newRow .= Xml::tags( 'td', null, $desc );
- $hitcount = wfMsg( 'tags-hitcount', $hitcount );
- $hitcount = $sk->link( SpecialPage::getTitleFor( 'RecentChanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) );
+ $hitcount = wfMsgExt( 'tags-hitcount', array( 'parsemag' ), $wgLang->formatNum( $hitcount ) );
+ $hitcount = $sk->link( SpecialPage::getTitleFor( 'Recentchanges' ), $hitcount, array(), array( 'tagfilter' => $tag ) );
$newRow .= Xml::tags( 'td', null, $hitcount );
$doneTags[] = $tag;
diff --git a/includes/specials/SpecialUncategorizedtemplates.php b/includes/specials/SpecialUncategorizedtemplates.php
index cb2a6d40..7e6fd24b 100644
--- a/includes/specials/SpecialUncategorizedtemplates.php
+++ b/includes/specials/SpecialUncategorizedtemplates.php
@@ -23,8 +23,6 @@ class UncategorizedTemplatesPage extends UncategorizedPagesPage {
/**
* Main execution point
- *
- * @param mixed $par Parameter passed to the page
*/
function wfSpecialUncategorizedtemplates() {
list( $limit, $offset ) = wfCheckLimits();
diff --git a/includes/specials/SpecialUndelete.php b/includes/specials/SpecialUndelete.php
index d97efb59..4db4e633 100644
--- a/includes/specials/SpecialUndelete.php
+++ b/includes/specials/SpecialUndelete.php
@@ -58,16 +58,15 @@ class PageArchive {
$title = Title::newFromText( $prefix );
if( $title ) {
$ns = $title->getNamespace();
- $encPrefix = $dbr->escapeLike( $title->getDBkey() );
+ $prefix = $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%'",
+ 'ar_title' . $dbr->buildLike( $prefix, $dbr->anyString() ),
);
return self::listPages( $dbr, $conds );
}
@@ -188,20 +187,7 @@ class PageArchive {
'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) );
+ return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleId() ) );
} else {
return null;
}
@@ -299,7 +285,7 @@ class PageArchive {
if( $row ) {
return $this->getTextFromRow( $row );
} else {
- return NULL;
+ return null;
}
}
@@ -345,7 +331,7 @@ class PageArchive {
}
if( $restoreText ) {
- $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress );
+ $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
if($textRestored === false) // It must be one of UNDELETE_*
return false;
} else {
@@ -372,7 +358,7 @@ class PageArchive {
}
if( trim( $comment ) != '' )
- $reason .= ": {$comment}";
+ $reason .= wfMsgForContent( 'colon-separator' ) . $comment;
$log->addEntry( 'restore', $this->title, $reason );
return array($textRestored, $filesRestored, $reason);
@@ -390,7 +376,7 @@ class PageArchive {
*
* @return mixed number of revisions restored or false on failure
*/
- private function undeleteRevisions( $timestamps, $unsuppress = false ) {
+ private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
if ( wfReadOnly() )
return false;
$restoreAll = empty( $timestamps );
@@ -399,13 +385,14 @@ class PageArchive {
# Does this page already exist? We'll have to update it...
$article = new Article( $this->title );
- $options = 'FOR UPDATE';
+ $options = 'FOR UPDATE'; // lock page
$page = $dbw->selectRow( 'page',
array( 'page_id', 'page_latest' ),
array( 'page_namespace' => $this->title->getNamespace(),
'page_title' => $this->title->getDBkey() ),
__METHOD__,
- $options );
+ $options
+ );
if( $page ) {
$makepage = false;
# Page already exists. Import the history, and if necessary
@@ -462,50 +449,53 @@ class PageArchive {
$oldones ),
__METHOD__,
/* options */ array( 'ORDER BY' => 'ar_timestamp' )
- );
+ );
$ret = $dbw->resultObject( $result );
$rev_count = $dbw->numRows( $result );
+ if( !$rev_count ) {
+ wfDebug( __METHOD__.": no revisions to restore\n" );
+ return false; // ???
+ }
+
+ $ret->seek( $rev_count - 1 ); // move to last
+ $row = $ret->fetchObject(); // get newest archived rev
+ $ret->seek( 0 ); // move back
if( $makepage ) {
+ // Check the state of the newest to-be version...
+ if( !$unsuppress && ($row->ar_deleted & Revision::DELETED_TEXT) ) {
+ return false; // we can't leave the current revision like this!
+ }
+ // Safe to insert now...
$newid = $article->insertOn( $dbw );
$pageId = $newid;
+ } else {
+ // Check if a deleted revision will become the current revision...
+ if( $row->ar_timestamp > $previousTimestamp ) {
+ // Check the state of the newest to-be version...
+ if( !$unsuppress && ($row->ar_deleted & Revision::DELETED_TEXT) ) {
+ return false; // we can't leave the current revision like this!
+ }
+ }
}
$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_' );
- }
// Check for key dupes due to shitty archive integrity.
if( $row->ar_rev_id ) {
$exists = $dbw->selectField( 'revision', '1', array('rev_id' => $row->ar_rev_id), __METHOD__ );
if( $exists ) continue; // don't throw DB errors
}
-
- $revision = new Revision( array(
- 'page' => $pageId,
- 'id' => $row->ar_rev_id,
- '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
+ // Insert one revision at a time...maintaining deletion status
+ // unless we are specifically removing all restrictions...
+ $revision = Revision::newFromArchiveRow( $row,
+ array(
+ 'page' => $pageId,
+ 'deleted' => $unsuppress ? 0 : $row->ar_deleted
) );
+
$revision->insertOn( $dbw );
$restored++;
@@ -529,21 +519,13 @@ class PageArchive {
if( $newid || $wasnew ) {
// Update site stats, link tables, etc
$article->createUpdates( $revision );
- // We don't handle well with top revision deleted
- if( $revision->getVisibility() ) {
- $dbw->update( 'revision',
- array( 'rev_deleted' => 0 ),
- array( 'rev_id' => $revision->getId() ),
- __METHOD__
- );
- }
}
if( $newid ) {
- wfRunHooks( 'ArticleUndelete', array( &$this->title, true ) );
+ wfRunHooks( 'ArticleUndelete', array( &$this->title, true, $comment ) );
Article::onArticleCreate( $this->title );
} else {
- wfRunHooks( 'ArticleUndelete', array( &$this->title, false ) );
+ wfRunHooks( 'ArticleUndelete', array( &$this->title, false, $comment ) );
Article::onArticleEdit( $this->title );
}
@@ -569,7 +551,7 @@ class PageArchive {
*/
class UndeleteForm {
var $mAction, $mTarget, $mTimestamp, $mRestore, $mInvert, $mTargetObj;
- var $mTargetTimestamp, $mAllowed, $mComment, $mToken;
+ var $mTargetTimestamp, $mAllowed, $mCanView, $mComment, $mToken;
function UndeleteForm( $request, $par = "" ) {
global $wgUser;
@@ -594,16 +576,21 @@ class UndeleteForm {
$this->mTarget = $par;
}
if ( $wgUser->isAllowed( 'undelete' ) && !$wgUser->isBlocked() ) {
- $this->mAllowed = true;
- } else {
+ $this->mAllowed = true; // user can restore
+ $this->mCanView = true; // user can view content
+ } elseif ( $wgUser->isAllowed( 'deletedtext' ) ) {
+ $this->mAllowed = false; // user cannot restore
+ $this->mCanView = true; // user can view content
+ } else { // user can only view the list of revisions
$this->mAllowed = false;
+ $this->mCanView = false;
$this->mTimestamp = '';
$this->mRestore = false;
}
if ( $this->mTarget !== "" ) {
$this->mTargetObj = Title::newFromURL( $this->mTarget );
} else {
- $this->mTargetObj = NULL;
+ $this->mTargetObj = null;
}
if( $this->mRestore || $this->mInvert ) {
$timestamps = array();
@@ -642,7 +629,7 @@ class UndeleteForm {
$this->showList( $result );
}
} else {
- $wgOut->addWikiText( wfMsgHtml( 'undelete-header' ) );
+ $wgOut->addWikiMsg( 'undelete-header' );
}
return;
}
@@ -652,8 +639,15 @@ class UndeleteForm {
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' );
+ if ( !$file->exists() ) {
+ $wgOut->addWikiMsg( 'filedelete-nofile', $this->mFile );
+ return;
+ } else if( !$file->userCan( File::DELETED_FILE ) ) {
+ if( $file->isDeleted( File::DELETED_RESTRICTED ) ) {
+ $wgOut->permissionRequired( 'suppressrevision' );
+ } else {
+ $wgOut->permissionRequired( 'deletedtext' );
+ }
return false;
} elseif ( !$wgUser->matchEditToken( $this->mToken, $this->mFile ) ) {
$this->showFileConfirmationForm( $this->mFile );
@@ -663,6 +657,11 @@ class UndeleteForm {
}
}
if( $this->mRestore && $this->mAction == "submit" ) {
+ global $wgUploadMaintenance;
+ if( $wgUploadMaintenance && $this->mTargetObj && $this->mTargetObj->getNamespace() == NS_FILE ) {
+ $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n", array( 'filedelete-maintenance' ) );
+ return;
+ }
return $this->undelete();
}
if( $this->mInvert && $this->mAction == "submit" ) {
@@ -707,8 +706,12 @@ class UndeleteForm {
$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() );
+ $link = $sk->linkKnown(
+ $undelete,
+ htmlspecialchars( $title->getPrefixedText() ),
+ array(),
+ array( 'target' => $title->getPrefixedText() )
+ );
$revs = wfMsgExt( 'undeleterevisions',
array( 'parseinline' ),
$wgLang->formatNum( $row->count ) );
@@ -741,14 +744,14 @@ class UndeleteForm {
return;
} else {
$wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
- $wgOut->addHTML( '<br/>' );
+ $wgOut->addHTML( '<br />' );
// and we are allowed to see...
}
}
$wgOut->setPageTitle( wfMsg( 'undeletepage' ) );
- $link = $skin->makeKnownLinkObj(
+ $link = $skin->linkKnown(
SpecialPage::getTitleFor( 'Undelete', $this->mTargetObj->getPrefixedDBkey() ),
htmlspecialchars( $this->mTargetObj->getPrefixedText() )
);
@@ -763,7 +766,7 @@ class UndeleteForm {
$wgOut->addHTML( '<hr />' );
}
} else {
- $wgOut->addHTML( wfMsgHtml( 'undelete-nodiff' ) );
+ $wgOut->addWikiMsg( 'undelete-nodiff' );
}
}
@@ -774,13 +777,36 @@ class UndeleteForm {
$t = htmlspecialchars( $wgLang->time( $timestamp, true ) );
$user = $skin->revUserTools( $rev );
- $wgOut->addHTML( '<p>' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</p>' );
+ if( $this->mPreview ) {
+ $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
+ } else {
+ $openDiv = '<div id="mw-undelete-revision">';
+ }
+
+ // Revision delete links
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $this->mDiff ) {
+ $revdlink = ''; // diffs already have revision delete links
+ } else if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) {
+ if( !$rev->userCan(Revision::DELETED_RESTRICTED ) ) {
+ $revdlink = $skin->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ } else {
+ $query = array(
+ 'type' => 'archive',
+ 'target' => $this->mTargetObj->getPrefixedDBkey(),
+ 'ids' => $rev->getTimestamp()
+ );
+ $revdlink = $skin->revDeleteLink( $query,
+ $rev->isDeleted( File::DELETED_RESTRICTED ), $canHide );
+ }
+ } else {
+ $revdlink = '';
+ }
+ $wgOut->addHTML( $openDiv . $revdlink . wfMsgWikiHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '</div>' );
wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
if( $this->mPreview ) {
- $wgOut->addHTML( "<hr />\n" );
-
//Hide [edit]s
$popts = $wgOut->parserOptions();
$popts->setEditSection( false );
@@ -797,7 +823,7 @@ class UndeleteForm {
Xml::openElement( 'div' ) .
Xml::openElement( 'form', array(
'method' => 'post',
- 'action' => $self->getLocalURL( "action=submit" ) ) ) .
+ 'action' => $self->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
Xml::element( 'input', array(
'type' => 'hidden',
'name' => 'target',
@@ -830,7 +856,7 @@ class UndeleteForm {
* @return string HTML
*/
function showDiff( $previousRev, $currentRev ) {
- global $wgOut, $wgUser;
+ global $wgOut;
$diffEngine = new DifferenceEngine();
$diffEngine->showDiffStyle();
@@ -852,39 +878,63 @@ class UndeleteForm {
$diffEngine->generateDiffBody(
$previousRev->getText(), $currentRev->getText() ) .
"</table>" .
- "</div>\n" );
-
+ "</div>\n"
+ );
}
private function diffHeader( $rev, $prefix ) {
- global $wgUser, $wgLang, $wgLang;
+ global $wgUser, $wgLang;
$sk = $wgUser->getSkin();
$isDeleted = !( $rev->getId() && $rev->getTitle() );
if( $isDeleted ) {
- /// @fixme $rev->getTitle() is null for deleted revs...?
+ /// @todo Fixme: $rev->getTitle() is null for deleted revs...?
$targetPage = SpecialPage::getTitleFor( 'Undelete' );
- $targetQuery = 'target=' .
- $this->mTargetObj->getPrefixedUrl() .
- '&timestamp=' .
- wfTimestamp( TS_MW, $rev->getTimestamp() );
+ $targetQuery = array(
+ 'target' => $this->mTargetObj->getPrefixedText(),
+ 'timestamp' => wfTimestamp( TS_MW, $rev->getTimestamp() )
+ );
} else {
- /// @fixme getId() may return non-zero for deleted revs...
+ /// @todo Fixme getId() may return non-zero for deleted revs...
$targetPage = $rev->getTitle();
- $targetQuery = 'oldid=' . $rev->getId();
+ $targetQuery = array( 'oldid' => $rev->getId() );
+ }
+ // Add show/hide deletion links if available
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) {
+ $del = ' ';
+ if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
+ $del .= $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
+ } else {
+ $query = array(
+ 'type' => 'archive',
+ 'target' => $this->mTargetObj->getPrefixedDbkey(),
+ 'ids' => $rev->getTimestamp()
+ );
+ $del .= $sk->revDeleteLink( $query,
+ $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
+ }
+ } else {
+ $del = '';
}
return
'<div id="mw-diff-'.$prefix.'title1"><strong>' .
- $sk->makeLinkObj( $targetPage,
- wfMsgHtml( 'revisionasof',
- $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
- $targetQuery ) .
- ( $isDeleted ? ' ' . wfMsgHtml( 'deletedrev' ) : '' ) .
+ $sk->link(
+ $targetPage,
+ wfMsgHtml(
+ 'revisionasof',
+ htmlspecialchars( $wgLang->timeanddate( $rev->getTimestamp(), true ) ),
+ htmlspecialchars( $wgLang->date( $rev->getTimestamp(), true ) ),
+ htmlspecialchars( $wgLang->time( $rev->getTimestamp(), true ) )
+ ),
+ array(),
+ $targetQuery
+ ) .
'</strong></div>' .
'<div id="mw-diff-'.$prefix.'title2">' .
- $sk->revUserTools( $rev ) . '<br/>' .
+ $sk->revUserTools( $rev ) . '<br />' .
'</div>' .
'<div id="mw-diff-'.$prefix.'title3">' .
- $sk->revComment( $rev ) . '<br/>' .
+ $sk->revComment( $rev ) . $del . '<br />' .
'</div>';
}
@@ -927,8 +977,11 @@ class UndeleteForm {
$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 );
+ global $IP;
+ require_once( "$IP/includes/StreamFile.php" );
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $path = $repo->getZonePath( 'deleted' ) . '/' . $repo->getDeletedHashPath( $key ) . $key;
+ wfStreamFile( $path );
}
private function showHistory( ) {
@@ -941,7 +994,7 @@ class UndeleteForm {
$wgOut->setPagetitle( wfMsg( 'viewdeletedpage' ) );
}
- $wgOut->addWikiText( wfMsgHtml( 'undeletepagetitle', $this->mTargetObj->getPrefixedText()) );
+ $wgOut->wrapWikiMsg( "<div class='mw-undelete-pagetitle'>\n$1</div>\n", array ( 'undeletepagetitle', $this->mTargetObj->getPrefixedText() ) );
$archive = new PageArchive( $this->mTargetObj );
/*
@@ -951,12 +1004,14 @@ class UndeleteForm {
return;
}
*/
+ $wgOut->addHTML( '<div class="mw-undelete-history">' );
if ( $this->mAllowed ) {
$wgOut->addWikiMsg( "undeletehistory" );
$wgOut->addWikiMsg( "undeleterevdel" );
} else {
$wgOut->addWikiMsg( "undeletehistorynoadmin" );
}
+ $wgOut->addHTML( '</div>' );
# List all stored revisions
$revisions = $archive->listRevisions();
@@ -987,7 +1042,7 @@ class UndeleteForm {
if ( $this->mAllowed ) {
$titleObj = SpecialPage::getTitleFor( "Undelete" );
- $action = $titleObj->getLocalURL( "action=submit" );
+ $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) );
# Start the form here
$top = Xml::openElement( 'form', array( 'method' => 'post', 'action' => $action, 'id' => 'undelete' ) );
$wgOut->addHTML( $top );
@@ -1021,7 +1076,7 @@ class UndeleteForm {
Xml::fieldset( wfMsg( 'undelete-fieldset-title' ) ) .
Xml::openElement( 'table', array( 'id' => 'mw-undelete-table' ) ) .
"<tr>
- <td colspan='2'>" .
+ <td colspan='2' class='mw-undelete-extrahelp'>" .
wfMsgWikiHtml( 'undeleteextrahelp' ) .
"</td>
</tr>
@@ -1091,20 +1146,13 @@ class UndeleteForm {
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 ) );
-
+ $rev = Revision::newFromArchiveRow( $row,
+ array( 'page' => $this->mTargetObj->getArticleId() ) );
$stxt = '';
$ts = wfTimestamp( TS_MW, $row->ar_timestamp );
+ // Build checkboxen...
if( $this->mAllowed ) {
- if( $this->mInvert){
+ if( $this->mInvert ) {
if( in_array( $ts, $this->mTargetTimestamp ) ) {
$checkBox = Xml::check( "ts$ts");
} else {
@@ -1113,41 +1161,61 @@ class UndeleteForm {
} else {
$checkBox = Xml::check( "ts$ts" );
}
+ } else {
+ $checkBox = '';
+ }
+ // Build page & diff links...
+ if( $this->mCanView ) {
$titleObj = SpecialPage::getTitleFor( "Undelete" );
- $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
# Last link
if( !$rev->userCan( Revision::DELETED_TEXT ) ) {
+ $pageLink = htmlspecialchars( $wgLang->timeanddate( $ts, true ) );
$last = wfMsgHtml('diff');
} else if( $remaining > 0 || ($earliestLiveTime && $ts > $earliestLiveTime) ) {
- $last = $sk->makeKnownLinkObj( $titleObj, wfMsgHtml('diff'),
- "target=" . $this->mTargetObj->getPrefixedUrl() . "&timestamp=$ts&diff=prev" );
+ $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
+ $last = $sk->linkKnown(
+ $titleObj,
+ wfMsgHtml('diff'),
+ array(),
+ array(
+ 'target' => $this->mTargetObj->getPrefixedText(),
+ 'timestamp' => $ts,
+ 'diff' => 'prev'
+ )
+ );
} else {
+ $pageLink = $this->getPageLink( $rev, $titleObj, $ts, $sk );
$last = wfMsgHtml('diff');
}
} else {
- $checkBox = '';
- $pageLink = $wgLang->timeanddate( $ts, true );
+ $pageLink = htmlspecialchars( $wgLang->timeanddate( $ts, true ) );
$last = wfMsgHtml('diff');
}
+ // User links
$userLink = $sk->revUserTools( $rev );
-
- if(!is_null($size = $row->ar_len)) {
+ // Revision text size
+ if( !is_null($size = $row->ar_len) ) {
$stxt = $sk->formatRevisionSize( $size );
}
+ // Edit summary
$comment = $sk->revComment( $rev );
- $revdlink = '';
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ // Revision delete links
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) {
if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $revdlink = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml('rev-delundel').')' );
+ $revdlink = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
} else {
- $query = array( 'target' => $this->mTargetObj->getPrefixedDBkey(),
- 'artimestamp[]' => $ts
+ $query = array(
+ 'type' => 'archive',
+ 'target' => $this->mTargetObj->getPrefixedDBkey(),
+ 'ids' => $ts
);
- $revdlink = $sk->revDeleteLink( $query, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
+ $revdlink = $sk->revDeleteLink( $query,
+ $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
}
+ } else {
+ $revdlink = '';
}
-
return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
}
@@ -1177,17 +1245,22 @@ class UndeleteForm {
')';
$data = htmlspecialchars( $data );
$comment = $this->getFileComment( $file, $sk );
- $revdlink = '';
- if( $wgUser->isAllowed( 'deleterevision' ) ) {
+ // Add show/hide deletion links if available
+ $canHide = $wgUser->isAllowed( 'deleterevision' );
+ if( $canHide || ($file->getVisibility() && $wgUser->isAllowed('deletedhistory')) ) {
if( !$file->userCan(File::DELETED_RESTRICTED ) ) {
- // If revision was hidden from sysops
- $revdlink = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.wfMsgHtml('rev-delundel').')' );
+ $revdlink = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
} else {
- $query = array( 'target' => $this->mTargetObj->getPrefixedDBkey(),
- 'fileid' => $row->fa_id
+ $query = array(
+ 'type' => 'filearchive',
+ 'target' => $this->mTargetObj->getPrefixedDBkey(),
+ 'ids' => $row->fa_id
);
- $revdlink = $sk->revDeleteLink( $query, $file->isDeleted( File::DELETED_RESTRICTED ) );
+ $revdlink = $sk->revDeleteLink( $query,
+ $file->isDeleted( File::DELETED_RESTRICTED ), $canHide );
}
+ } else {
+ $revdlink = '';
}
return "<li>$checkBox $revdlink $pageLink . . $userLink $data $comment</li>\n";
}
@@ -1199,11 +1272,20 @@ class UndeleteForm {
function getPageLink( $rev, $titleObj, $ts, $sk ) {
global $wgLang;
+ $time = htmlspecialchars( $wgLang->timeanddate( $ts, true ) );
+
if( !$rev->userCan(Revision::DELETED_TEXT) ) {
- return '<span class="history-deleted">' . $wgLang->timeanddate( $ts, true ) . '</span>';
+ return '<span class="history-deleted">' . $time . '</span>';
} else {
- $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ),
- "target=".$this->mTargetObj->getPrefixedUrl()."&timestamp=$ts" );
+ $link = $sk->linkKnown(
+ $titleObj,
+ $time,
+ array(),
+ array(
+ 'target' => $this->mTargetObj->getPrefixedText(),
+ 'timestamp' => $ts
+ )
+ );
if( $rev->isDeleted(Revision::DELETED_TEXT) )
$link = '<span class="history-deleted">' . $link . '</span>';
return $link;
@@ -1220,10 +1302,16 @@ class UndeleteForm {
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" .
- "&token=" . urlencode( $wgUser->editToken( $key ) ) );
+ $link = $sk->linkKnown(
+ $titleObj,
+ $wgLang->timeanddate( $ts, true ),
+ array(),
+ array(
+ 'target' => $this->mTargetObj->getPrefixedText(),
+ 'file' => $key,
+ 'token' => $wgUser->editToken( $key )
+ )
+ );
if( $file->isDeleted(File::DELETED_FILE) )
$link = '<span class="history-deleted">' . $link . '</span>';
return $link;
@@ -1282,7 +1370,7 @@ class UndeleteForm {
$wgUser, $this->mComment) );
$skin = $wgUser->getSkin();
- $link = $skin->makeKnownLinkObj( $this->mTargetObj );
+ $link = $skin->linkKnown( $this->mTargetObj );
$wgOut->addHTML( wfMsgWikiHtml( 'undeletedpage', $link ) );
} else {
$wgOut->showFatalError( wfMsg( "cannotundelete" ) );
diff --git a/includes/specials/SpecialUnlockdb.php b/includes/specials/SpecialUnlockdb.php
index a3e8a0c4..fe38a48a 100644
--- a/includes/specials/SpecialUnlockdb.php
+++ b/includes/specials/SpecialUnlockdb.php
@@ -45,7 +45,7 @@ class DBUnlockForm {
$wgOut->setPagetitle( wfMsg( "unlockdb" ) );
$wgOut->addWikiMsg( "unlockdbtext" );
- if ( "" != $err ) {
+ if ( $err != "" ) {
$wgOut->setSubtitle( wfMsg( "formerror" ) );
$wgOut->addHTML( '<p class="error">' . htmlspecialchars( $err ) . "</p>\n" );
}
@@ -55,7 +55,7 @@ class DBUnlockForm {
$action = $titleObj->escapeLocalURL( "action=submit" );
$token = htmlspecialchars( $wgUser->editToken() );
- $wgOut->addHTML( <<<END
+ $wgOut->addHTML( <<<HTML
<form id="unlockdb" method="post" action="{$action}">
<table border="0">
@@ -74,7 +74,7 @@ class DBUnlockForm {
</table>
<input type="hidden" name="wpEditToken" value="{$token}" />
</form>
-END
+HTML
);
}
diff --git a/includes/specials/SpecialUnusedcategories.php b/includes/specials/SpecialUnusedcategories.php
index 406f7944..fe7d7a17 100644
--- a/includes/specials/SpecialUnusedcategories.php
+++ b/includes/specials/SpecialUnusedcategories.php
@@ -34,7 +34,7 @@ class UnusedCategoriesPage extends QueryPage {
function formatResult( $skin, $result ) {
$title = Title::makeTitle( NS_CATEGORY, $result->title );
- return $skin->makeLinkObj( $title, $title->getText() );
+ return $skin->link( $title, $title->getText() );
}
}
diff --git a/includes/specials/SpecialUnusedimages.php b/includes/specials/SpecialUnusedimages.php
index fa66555d..9d9868f6 100644
--- a/includes/specials/SpecialUnusedimages.php
+++ b/includes/specials/SpecialUnusedimages.php
@@ -25,9 +25,19 @@ class UnusedimagesPage extends ImageQueryPage {
global $wgCountCategorizedImagesAsUsed, $wgDBtype;
$dbr = wfGetDB( DB_SLAVE );
- $epoch = $wgDBtype == 'mysql' ?
- 'UNIX_TIMESTAMP(img_timestamp)' :
- 'EXTRACT(epoch FROM img_timestamp)';
+ switch ($wgDBtype) {
+ case 'mysql':
+ $epoch = 'UNIX_TIMESTAMP(img_timestamp)';
+ break;
+ case 'oracle':
+ $epoch = '((trunc(img_timestamp) - to_date(\'19700101\',\'YYYYMMDD\')) * 86400)';
+ break;
+ case 'sqlite':
+ $epoch = 'img_timestamp';
+ break;
+ default:
+ $epoch = 'EXTRACT(epoch FROM img_timestamp)';
+ }
if ( $wgCountCategorizedImagesAsUsed ) {
list( $page, $image, $imagelinks, $categorylinks ) = $dbr->tableNamesN( 'page', 'image', 'imagelinks', 'categorylinks' );
diff --git a/includes/specials/SpecialUnusedtemplates.php b/includes/specials/SpecialUnusedtemplates.php
index 89acd09c..6ddbab32 100644
--- a/includes/specials/SpecialUnusedtemplates.php
+++ b/includes/specials/SpecialUnusedtemplates.php
@@ -33,11 +33,18 @@ class UnusedtemplatesPage extends QueryPage {
function formatResult( $skin, $result ) {
$title = Title::makeTitle( NS_TEMPLATE, $result->title );
- $pageLink = $skin->makeKnownLinkObj( $title, '', 'redirect=no' );
- $wlhLink = $skin->makeKnownLinkObj(
+ $pageLink = $skin->linkKnown(
+ $title,
+ null,
+ array(),
+ array( 'redirect' => 'no' )
+ );
+ $wlhLink = $skin->linkKnown(
SpecialPage::getTitleFor( 'Whatlinkshere' ),
wfMsgHtml( 'unusedtemplateswlh' ),
- 'target=' . $title->getPrefixedUrl() );
+ array(),
+ array( 'target' => $title->getPrefixedText() )
+ );
return wfSpecialList( $pageLink, $wlhLink );
}
diff --git a/includes/specials/SpecialUnwatchedpages.php b/includes/specials/SpecialUnwatchedpages.php
index 64ab3729..483afdaa 100644
--- a/includes/specials/SpecialUnwatchedpages.php
+++ b/includes/specials/SpecialUnwatchedpages.php
@@ -44,8 +44,16 @@ class UnwatchedpagesPage extends QueryPage {
$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' );
+ $plink = $skin->linkKnown(
+ $nt,
+ htmlspecialchars( $text )
+ );
+ $wlink = $skin->linkKnown(
+ $nt,
+ wfMsgHtml( 'watch' ),
+ array(),
+ array( 'action' => 'watch' )
+ );
return wfSpecialList( $plink, $wlink );
}
diff --git a/includes/specials/SpecialUpload.php b/includes/specials/SpecialUpload.php
index 4c5bb160..9569945d 100644
--- a/includes/specials/SpecialUpload.php
+++ b/includes/specials/SpecialUpload.php
@@ -2,250 +2,139 @@
/**
* @file
* @ingroup SpecialPage
+ * @ingroup Upload
+ *
+ * Form for handling uploads and special page.
+ *
*/
-
-/**
- * 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_LENGTH_PARTNAME = 4;
- const ILLEGAL_FILENAME = 5;
- const PROTECTED_PAGE = 6;
- const OVERWRITE_EXISTING_FILE = 7;
- const FILETYPE_MISSING = 8;
- const FILETYPE_BADTYPE = 9;
- const VERIFICATION_ERROR = 10;
- const UPLOAD_VERIFICATION_ERROR = 11;
- const UPLOAD_WARNING = 12;
- const INTERNAL_ERROR = 13;
-
- /**#@+
- * @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;
- /**#@-*/
-
+class SpecialUpload extends SpecialPage {
/**
* Constructor : initialise object
* Get data POSTed through the form and assign them to the object
- * @param $request Data posted.
+ * @param WebRequest $request Data posted.
*/
- function UploadForm( &$request ) {
- global $wgAllowCopyUploads;
- $this->mDesiredDestName = $request->getText( 'wpDestFile' );
- $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' );
- $this->mComment = $request->getText( 'wpUploadDescription' );
- $this->mForReUpload = $request->getBool( 'wpForReUpload' );
- $this->mReUpload = $request->getCheck( 'wpReUpload' );
-
- if( !$request->wasPosted() ) {
- # GET requests just give the main form; no data except destination
- # filename and description
- return;
- }
+ public function __construct( $request = null ) {
+ global $wgRequest;
- # Placeholders for text injection by hooks (empty per default)
- $this->uploadFormTextTop = "";
- $this->uploadFormTextAfterSummary = "";
- $this->mUploadClicked = $request->getCheck( 'wpUpload' );
+ parent::__construct( 'Upload', 'upload' );
- $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 );
- }
- }
+ $this->loadRequest( is_null( $request ) ? $wgRequest : $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
- }
+ /** Misc variables **/
+ protected $mRequest; // The WebRequest or FauxRequest this form is supposed to handle
+ protected $mSourceType;
+ protected $mUpload;
+ protected $mLocalFile;
+ protected $mUploadClicked;
+
+ /** User input variables from the "description" section **/
+ public $mDesiredDestName; // The requested target file name
+ protected $mComment;
+ protected $mLicense;
+
+ /** User input variables from the root section **/
+ protected $mIgnoreWarning;
+ protected $mWatchThis;
+ protected $mCopyrightStatus;
+ protected $mCopyrightSource;
+
+ /** Hidden variables **/
+ protected $mDestWarningAck;
+ protected $mForReUpload; // The user followed an "overwrite this file" link
+ protected $mCancelUpload; // The user clicked "Cancel and return to upload form" button
+ protected $mTokenOk;
+ protected $mUploadSuccessful = false; // Subclasses can use this to determine whether a file was uploaded
+
+ /** Text injection points for hooks not using HTMLForm **/
+ public $uploadFormTextTop;
+ public $uploadFormTextAfterSummary;
+
/**
- * Copy a web file to a temporary file
- * @access private
+ * Initialize instance variables from request and create an Upload handler
+ *
+ * @param WebRequest $request The request to extract variables from
*/
- 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 );
- }
+ protected function loadRequest( $request ) {
+ global $wgUser;
- /**
- * Safe copy from URL
- * Returns true if there was an error, false otherwise
- */
- private function curlCopy( $url, $dest ) {
- global $wgUser, $wgOut, $wgHTTPProxy;
+ $this->mRequest = $request;
+ $this->mSourceType = $request->getVal( 'wpSourceType', 'file' );
+ $this->mUpload = UploadBase::createFromRequest( $request );
+ $this->mUploadClicked = $request->wasPosted()
+ && ( $request->getCheck( 'wpUpload' )
+ || $request->getCheck( 'wpUploadIgnoreWarning' ) );
- if( !$wgUser->isAllowed( 'upload_by_url' ) ) {
- $wgOut->permissionRequired( 'upload_by_url' );
- return true;
- }
+ // Guess the desired name from the filename if not provided
+ $this->mDesiredDestName = $request->getText( 'wpDestFile' );
+ if( !$this->mDesiredDestName && $request->getFileName( 'wpUploadFile' ) !== null )
+ $this->mDesiredDestName = $request->getFileName( 'wpUploadFile' );
+ $this->mComment = $request->getText( 'wpUploadDescription' );
+ $this->mLicense = $request->getText( 'wpLicense' );
- # 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;
- }
+ $this->mDestWarningAck = $request->getText( 'wpDestFileWarningAck' );
+ $this->mIgnoreWarning = $request->getCheck( 'wpIgnoreWarning' )
+ || $request->getCheck( 'wpUploadIgnoreWarning' );
+ $this->mWatchthis = $request->getBool( 'wpWatchthis' ) && $wgUser->isLoggedIn();
+ $this->mCopyrightStatus = $request->getText( 'wpUploadCopyStatus' );
+ $this->mCopyrightSource = $request->getText( 'wpUploadSource' );
- $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);
- if( $wgHTTPProxy ) {
- curl_setopt( $ch, CURLOPT_PROXY, $wgHTTPProxy );
- }
- 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;
+ $this->mForReUpload = $request->getBool( 'wpForReUpload' ); // updating a file
+ $this->mCancelUpload = $request->getCheck( 'wpCancelUpload' )
+ || $request->getCheck( 'wpReUpload' ); // b/w compat
+
+ // If it was posted check for the token (no remote POST'ing with user credentials)
+ $token = $request->getVal( 'wpEditToken' );
+ if( $this->mSourceType == 'file' && $token == null ) {
+ // Skip token check for file uploads as that can't be faked via JS...
+ // Some client-side tools don't expect to need to send wpEditToken
+ // with their submissions, as that's new in 1.16.
+ $this->mTokenOk = true;
+ } else {
+ $this->mTokenOk = $wgUser->matchEditToken( $token );
+ }
+
+ $this->uploadFormTextTop = '';
+ $this->uploadFormTextAfterSummary = '';
}
/**
- * Callback function for CURL-based web transfer
- * Write data to file unless we've passed the length limit;
- * if so, abort immediately.
- * @access private
+ * This page can be shown if uploading is enabled.
+ * Handle permission checking elsewhere in order to be able to show
+ * custom error messages.
+ *
+ * @param User $user
+ * @return bool
*/
- function uploadCurlCallback( $ch, $data ) {
- global $wgMaxUploadSize;
- $length = strlen( $data );
- $this->mFileSize += $length;
- if( $this->mFileSize > $wgMaxUploadSize ) {
- return 0;
- }
- fwrite( $this->mCurlDestHandle, $data );
- return $length;
+ public function userCanExecute( $user ) {
+ return UploadBase::isEnabled() && parent::userCanExecute( $user );
}
/**
- * Start doing stuff
- * @access public
+ * Special page entry point
*/
- function execute() {
- global $wgUser, $wgOut;
- global $wgEnableUploads;
+ public function execute( $par ) {
+ global $wgUser, $wgOut, $wgRequest;
- # Check php's file_uploads setting
- if( !wfIniGetBool( 'file_uploads' ) ) {
- $wgOut->showErrorPage( 'uploaddisabled', 'php-uploaddisabledtext', array( $this->mDesiredDestName ) );
- return;
- }
+ $this->setHeaders();
+ $this->outputHeader();
# Check uploading enabled
- if( !$wgEnableUploads ) {
- $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext', array( $this->mDesiredDestName ) );
+ if( !UploadBase::isEnabled() ) {
+ $wgOut->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' );
return;
}
# Check permissions
+ global $wgGroupPermissions;
if( !$wgUser->isAllowed( 'upload' ) ) {
- if( !$wgUser->isLoggedIn() ) {
+ if( !$wgUser->isLoggedIn() && ( $wgGroupPermissions['user']['upload']
+ || $wgGroupPermissions['autoconfirmed']['upload'] ) ) {
+ // Custom message if logged-in users without any special rights can upload
$wgOut->showErrorPage( 'uploadnologin', 'uploadnologintext' );
} else {
$wgOut->permissionRequired( 'upload' );
@@ -259,459 +148,490 @@ class UploadForm {
return;
}
+ # Check whether we actually want to allow changing stuff
if( wfReadOnly() ) {
$wgOut->readOnlyPage();
return;
}
- if( $this->mReUpload ) {
- if( !$this->unsaveUploadedFile() ) {
+ # Unsave the temporary file in case this was a cancelled upload
+ if ( $this->mCancelUpload ) {
+ if ( !$this->unsaveUploadedFile() )
+ # Something went wrong, so unsaveUploadedFile showed a warning
return;
- }
- # Because it is probably checked and shouldn't be
- $this->mIgnoreWarning = false;
-
- $this->mainUploadForm();
- } else if( 'submit' == $this->mAction || $this->mUploadClicked ) {
+ }
+
+ # Process upload or show a form
+ if ( $this->mTokenOk && !$this->mCancelUpload
+ && ( $this->mUpload && $this->mUploadClicked ) ) {
$this->processUpload();
} else {
- $this->mainUploadForm();
+ # Backwards compatibility hook
+ if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) )
+ {
+ wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" );
+ return;
+ }
+
+ $this->showUploadForm( $this->getUploadForm() );
}
- $this->cleanupTempFile();
+ # Cleanup
+ if ( $this->mUpload )
+ $this->mUpload->cleanupTempFile();
}
/**
- * Do the upload
- * Checks are made in SpecialUpload::execute()
+ * Show the main upload form
*
- * @access private
+ * @param mixed $form An HTMLForm instance or HTML string to show
*/
- 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_LENGTH_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;
+ protected function showUploadForm( $form ) {
+ # Add links if file was previously deleted
+ if ( !$this->mDesiredDestName ) {
+ $this->showViewDeletedLinks();
+ }
+
+ if ( $form instanceof HTMLForm ) {
+ $form->show();
+ } else {
+ global $wgOut;
+ $wgOut->addHTML( $form );
+ }
+
+ }
- case self::OVERWRITE_EXISTING_FILE:
- $errorText = $details['overwrite'];
- $this->uploadError( $wgOut->parse( $errorText ) );
- break;
+ /**
+ * Get an UploadForm instance with title and text properly set.
+ *
+ * @param string $message HTML string to add to the form
+ * @param string $sessionKey Session key in case this is a stashed upload
+ * @return UploadForm
+ */
+ protected function getUploadForm( $message = '', $sessionKey = '', $hideIgnoreWarning = false ) {
+ global $wgOut;
+
+ # Initialize form
+ $form = new UploadForm( array(
+ 'watch' => $this->getWatchCheck(),
+ 'forreupload' => $this->mForReUpload,
+ 'sessionkey' => $sessionKey,
+ 'hideignorewarning' => $hideIgnoreWarning,
+ 'destwarningack' => (bool)$this->mDestWarningAck,
+
+ 'texttop' => $this->uploadFormTextTop,
+ 'textaftersummary' => $this->uploadFormTextAfterSummary,
+ 'destfile' => $this->mDesiredDestName,
+ ) );
+ $form->setTitle( $this->getTitle() );
+
+ # Check the token, but only if necessary
+ if( !$this->mTokenOk && !$this->mCancelUpload
+ && ( $this->mUpload && $this->mUploadClicked ) ) {
+ $form->addPreText( wfMsgExt( 'session_fail_preview', 'parseinline' ) );
+ }
+
+ # Add text to form
+ $form->addPreText( '<div id="uploadtext">' .
+ wfMsgExt( 'uploadtext', 'parse', array( $this->mDesiredDestName ) ) .
+ '</div>' );
+ # Add upload error message
+ $form->addPreText( $message );
+
+ # Add footer to form
+ $uploadFooter = wfMsgNoTrans( 'uploadfooter' );
+ if ( $uploadFooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadFooter ) ) {
+ $form->addPostText( '<div id="mw-upload-footer-message">'
+ . $wgOut->parse( $uploadFooter ) . "</div>\n" );
+ }
+
+ return $form;
- 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 ),
- $wgLang->commaList( $wgFileExtensions ),
- $wgLang->formatNum( count($wgFileExtensions) )
+ /**
+ * Shows the "view X deleted revivions link""
+ */
+ protected function showViewDeletedLinks() {
+ global $wgOut, $wgUser;
+
+ $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
+ // Show a subtitle link to deleted revisions (to sysops et al only)
+ if( $title instanceof Title ) {
+ $count = $title->isDeleted();
+ if ( $count > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) {
+ $link = wfMsgExt(
+ $wgUser->isAllowed( 'delete' ) ? 'thisisdeleted' : 'viewdeleted',
+ array( 'parse', 'replaceafter' ),
+ $wgUser->getSkin()->linkKnown(
+ SpecialPage::getTitleFor( 'Undelete', $title->getPrefixedText() ),
+ wfMsgExt( 'restorelink', array( 'parsemag', 'escape' ), $count )
)
);
- 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;
+ $wgOut->addHTML( "<div id=\"contentSub2\">{$link}</div>" );
+ }
+ }
- default:
- throw new MWException( __METHOD__ . ": Unknown value `{$value}`" );
- }
+ // Show the relevant lines from deletion log (for still deleted files only)
+ if( $title instanceof Title && $title->isDeletedQuick() && !$title->exists() ) {
+ $this->showDeletionLog( $wgOut, $title->getPrefixedText() );
+ }
}
/**
- * Really do the upload
- * Checks are made in SpecialUpload::execute()
+ * Stashes the upload and shows the main upload form.
*
- * @param array $resultDetails contains result-specific dict of additional values
+ * Note: only errors that can be handled by changing the name or
+ * description should be redirected here. It should be assumed that the
+ * file itself is sane and has passed UploadBase::verifyFile. This
+ * essentially means that UploadBase::VERIFICATION_ERROR and
+ * UploadBase::EMPTY_FILE should not be passed here.
*
- * @access private
+ * @param string $message HTML message to be passed to mainUploadForm
+ */
+ protected function showRecoverableUploadError( $message ) {
+ $sessionKey = $this->mUpload->stashSession();
+ $message = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" .
+ '<div class="error">' . $message . "</div>\n";
+
+ $form = $this->getUploadForm( $message, $sessionKey );
+ $form->setSubmitText( wfMsg( 'upload-tryagain' ) );
+ $this->showUploadForm( $form );
+ }
+ /**
+ * Stashes the upload, shows the main form, but adds an "continue anyway button".
+ * Also checks whether there are actually warnings to display.
+ *
+ * @param array $warnings
+ * @return boolean true if warnings were displayed, false if there are no
+ * warnings and the should continue processing like there was no warning
*/
- function internalProcessUpload( &$resultDetails ) {
+ protected function showUploadWarning( $warnings ) {
global $wgUser;
- if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
+ # If there are no warnings, or warnings we can ignore, return early.
+ # mDestWarningAck is set when some javascript has shown the warning
+ # to the user. mForReUpload is set when the user clicks the "upload a
+ # new version" link.
+ if ( !$warnings || ( count( $warnings ) == 1 &&
+ isset( $warnings['exists'] ) &&
+ ( $this->mDestWarningAck || $this->mForReUpload ) ) )
{
- wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" );
- return self::BEFORE_PROCESSING;
+ return false;
}
- /**
- * If there was no filename or a zero size given, give up quick.
- */
- if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) {
- return self::EMPTY_FILE;
- }
+ $sessionKey = $this->mUpload->stashSession();
- /* Check for curl error */
- if( $this->mCurlError ) {
- return self::BEFORE_PROCESSING;
- }
+ $sk = $wgUser->getSkin();
- /**
- * 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 );
-
- /* Normalize to title form before we do any further processing */
- $nt = Title::makeTitleSafe( NS_FILE, $filtered );
- if( is_null( $nt ) ) {
- $resultDetails = array( 'filtered' => $filtered );
- return self::ILLEGAL_FILENAME;
+ $warningHtml = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n"
+ . '<ul class="warning">';
+ foreach( $warnings as $warning => $args ) {
+ $msg = '';
+ if( $warning == 'exists' ) {
+ $msg = "\t<li>" . self::getExistsWarning( $args ) . "</li>\n";
+ } elseif( $warning == 'duplicate' ) {
+ $msg = self::getDupeWarning( $args );
+ } elseif( $warning == 'duplicate-archive' ) {
+ $msg = "\t<li>" . wfMsgExt( 'file-deleted-duplicate', 'parseinline',
+ array( Title::makeTitle( NS_FILE, $args )->getPrefixedText() ) )
+ . "</li>\n";
+ } else {
+ if ( $args === true )
+ $args = array();
+ elseif ( !is_array( $args ) )
+ $args = array( $args );
+ $msg = "\t<li>" . wfMsgExt( $warning, 'parseinline', $args ) . "</li>\n";
+ }
+ $warningHtml .= $msg;
}
- $filtered = $nt->getDBkey();
+ $warningHtml .= "</ul>\n";
+ $warningHtml .= wfMsgExt( 'uploadwarning-text', 'parse' );
+
+ $form = $this->getUploadForm( $warningHtml, $sessionKey, /* $hideIgnoreWarning */ true );
+ $form->setSubmitText( wfMsg( 'upload-tryagain' ) );
+ $form->addButton( 'wpUploadIgnoreWarning', wfMsg( 'ignorewarning' ) );
+ $form->addButton( 'wpCancelUpload', wfMsg( 'reuploaddesc' ) );
+
+ $this->showUploadForm( $form );
- /**
- * 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 = '';
- }
+ # Indicate that we showed a form
+ return true;
+ }
- # 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];
- }
+ /**
+ * Show the upload form with error message, but do not stash the file.
+ *
+ * @param string $message
+ */
+ protected function showUploadError( $message ) {
+ $message = '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" .
+ '<div class="error">' . $message . "</div>\n";
+ $this->showUploadForm( $this->getUploadForm( $message ) );
+ }
- if( strlen( $partname ) < 1 ) {
- return self::MIN_LENGTH_PARTNAME;
- }
+ /**
+ * Do the upload.
+ * Checks are made in SpecialUpload::execute()
+ */
+ protected function processUpload() {
+ global $wgUser, $wgOut;
- $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;
+ // Verify permissions
+ $permErrors = $this->mUpload->verifyPermissions( $wgUser );
+ if( $permErrors !== true ) {
+ $wgOut->showPermissionsErrorPage( $permErrors );
+ return;
}
- /**
- * 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;
+ // Fetch the file if required
+ $status = $this->mUpload->fetchFile();
+ if( !$status->isOK() ) {
+ $this->showUploadForm( $this->getUploadForm( $wgOut->parse( $status->getWikiText() ) ) );
+ return;
}
- /* 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;
+ // Deprecated backwards compatibility hook
+ if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) )
+ {
+ wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" );
+ return array( 'status' => UploadBase::BEFORE_PROCESSING );
}
- /**
- * 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;
- }
+ // Upload verification
+ $details = $this->mUpload->verifyUpload();
+ if ( $details['status'] != UploadBase::OK ) {
+ $this->processVerificationError( $details );
+ return;
}
+ $this->mLocalFile = $this->mUpload->getLocalFile();
- /**
- * Check for non-fatal conditions
- */
- if ( ! $this->mIgnoreWarning ) {
- $warning = '';
-
- $comparableName = str_replace( ' ', '_', $basename );
- global $wgCapitalLinks, $wgContLang;
- if ( $wgCapitalLinks ) {
- $comparableName = $wgContLang->ucfirst( $comparableName );
- }
-
- if( $comparableName !== $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 ),
- $wgLang->commaList( $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, $finalExt, $nt );
-
- 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;
+ // Check warnings if necessary
+ if( !$this->mIgnoreWarning ) {
+ $warnings = $this->mUpload->checkWarnings();
+ if( $this->showUploadWarning( $warnings ) ) {
+ return;
}
}
- /**
- * Try actually saving the thing...
- * It will show an error form on failure.
- */
+ // Get the page text if this is not a reupload
if( !$this->mForReUpload ) {
$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;
+ $pageText = false;
}
+ $status = $this->mUpload->performUpload( $this->mComment, $pageText, $this->mWatchthis, $wgUser );
+ if ( !$status->isGood() ) {
+ $this->showUploadError( $wgOut->parse( $status->getWikiText() ) );
+ return;
+ }
+
+ // Success, redirect to description page
+ $this->mUploadSuccessful = true;
+ wfRunHooks( 'SpecialUploadComplete', array( &$this ) );
+ $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() );
+
}
/**
- * 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
+ * Get the initial image page text based on a comment and optional file status information
*/
- 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 = '';
+ public static function getInitialPageText( $comment = '', $license = '', $copyStatus = '', $source = '' ) {
+ global $wgUseCopyrightUpload;
+ if ( $wgUseCopyrightUpload ) {
+ $licensetxt = '';
+ if ( $license != '' ) {
+ $licensetxt = '== ' . wfMsgForContent( 'license-header' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ }
+ $pageText = '== ' . wfMsgForContent ( 'filedesc' ) . " ==\n" . $comment . "\n" .
+ '== ' . wfMsgForContent ( 'filestatus' ) . " ==\n" . $copyStatus . "\n" .
+ "$licensetxt" .
+ '== ' . wfMsgForContent ( 'filesource' ) . " ==\n" . $source ;
} else {
- $n = strrpos( $file->getName(), '.' );
- $rawExtension = substr( $file->getName(), $n + 1 );
- $partname = substr( $file->getName(), 0, $n );
+ if ( $license != '' ) {
+ $filedesc = $comment == '' ? '' : '== ' . wfMsgForContent ( 'filedesc' ) . " ==\n" . $comment . "\n";
+ $pageText = $filedesc .
+ '== ' . wfMsgForContent ( 'license-header' ) . " ==\n" . '{{' . $license . '}}' . "\n";
+ } else {
+ $pageText = $comment;
+ }
}
+ return $pageText;
+ }
- $sk = $wgUser->getSkin();
+ /**
+ * 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.
+ */
+ protected function getWatchCheck() {
+ global $wgUser;
+ if( $wgUser->getOption( 'watchdefault' ) ) {
+ // Watch all edits!
+ return true;
+ }
- if ( $rawExtension != $file->getExtension() ) {
- // We're not using the normalized form of the extension.
- // Normal form is lowercase, using most common of alternate
- // extensions (eg 'jpg' rather than 'JPEG').
- //
- // Check for another file using the normalized form...
- $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() );
- $file_lc = wfLocalFile( $nt_lc );
+ $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 {
- $file_lc = false;
+ // New page should get watched if that's our option.
+ return $wgUser->getOption( 'watchcreations' );
}
+ }
- 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 = '';
- }
+ /**
+ * Provides output to the user for a result of UploadBase::verifyUpload
+ *
+ * @param array $details Result of UploadBase::verifyUpload
+ */
+ protected function processVerificationError( $details ) {
+ global $wgFileExtensions, $wgLang;
- $warning .= '<li>' .
- wfMsgExt( 'fileexists-extension', 'parsemag',
- $file->getTitle()->getPrefixedText(), $dlink ) .
- '</li>' . $dlink2;
+ switch( $details['status'] ) {
- } 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>';
+ /** Statuses that only require name changing **/
+ case UploadBase::MIN_LENGTH_PARTNAME:
+ $this->showRecoverableUploadError( wfMsgHtml( 'minlength1' ) );
+ break;
+ case UploadBase::ILLEGAL_FILENAME:
+ $this->showRecoverableUploadError( wfMsgExt( 'illegalfilename',
+ 'parseinline', $details['filtered'] ) );
+ break;
+ case UploadBase::OVERWRITE_EXISTING_FILE:
+ $this->showRecoverableUploadError( wfMsgExt( $details['overwrite'],
+ 'parseinline' ) );
+ break;
+ case UploadBase::FILETYPE_MISSING:
+ $this->showRecoverableUploadError( wfMsgExt( 'filetype-missing',
+ 'parseinline' ) );
+ break;
+
+ /** Statuses that require reuploading **/
+ case UploadBase::EMPTY_FILE:
+ $this->showUploadForm( $this->getUploadForm( wfMsgHtml( 'emptyfile' ) ) );
+ break;
+ case UploadBase::FILETYPE_BADTYPE:
+ $finalExt = $details['finalExt'];
+ $this->showUploadError(
+ wfMsgExt( 'filetype-banned-type',
+ array( 'parseinline' ),
+ htmlspecialchars( $finalExt ),
+ implode(
+ wfMsgExt( 'comma-separator', array( 'escapenoentities' ) ),
+ $wgFileExtensions
+ ),
+ $wgLang->formatNum( count( $wgFileExtensions ) )
+ )
+ );
+ break;
+ case UploadBase::VERIFICATION_ERROR:
+ unset( $details['status'] );
+ $code = array_shift( $details['details'] );
+ $this->showUploadError( wfMsgExt( $code, 'parseinline', $details['details'] ) );
+ break;
+ case UploadBase::HOOK_ABORTED:
+ if ( is_array( $details['error'] ) ) { # allow hooks to return error details in an array
+ $args = $details['error'];
+ $error = array_shift( $args );
} else {
- $dlink2 = '';
+ $error = $details['error'];
+ $args = null;
}
- $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>';
- }
+ $this->showUploadError( wfMsgExt( $error, 'parseinline', $args ) );
+ break;
+ default:
+ throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" );
}
+ }
- $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;
- }
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @access private
+ * @return success
+ */
+ protected function unsaveUploadedFile() {
+ global $wgOut;
+ if ( !( $this->mUpload instanceof UploadFromStash ) )
+ return true;
+ $success = $this->mUpload->unsaveUploadedFile();
+ if ( ! $success ) {
+ $wgOut->showFileDeleteError( $this->mUpload->getTempPath() );
+ return false;
+ } else {
+ return true;
}
+ }
+
+ /*** Functions for formatting warnings ***/
+
+ /**
+ * Formats a result of UploadBase::getExistsWarning as HTML
+ * This check is static and can be done pre-upload via AJAX
+ *
+ * @param array $exists The result of UploadBase::getExistsWarning
+ * @return string Empty string if there is no warning or an HTML fragment
+ */
+ public static function getExistsWarning( $exists ) {
+ global $wgUser, $wgContLang;
+
+ if ( !$exists )
+ return '';
- if ( $file->wasDeleted() && !$file->exists() ) {
+ $file = $exists['file'];
+ $filename = $file->getTitle()->getPrefixedText();
+ $warning = '';
+
+ $sk = $wgUser->getSkin();
+
+ if( $exists['warning'] == 'exists' ) {
+ // Exact match
+ $warning = wfMsgExt( 'fileexists', 'parseinline', $filename );
+ } elseif( $exists['warning'] == 'page-exists' ) {
+ // Page exists but file does not
+ $warning = wfMsgExt( 'filepageexists', 'parseinline', $filename );
+ } elseif ( $exists['warning'] == 'exists-normalized' ) {
+ $warning = wfMsgExt( 'fileexists-extension', 'parseinline', $filename,
+ $exists['normalizedFile']->getTitle()->getPrefixedText() );
+ } elseif ( $exists['warning'] == 'thumb' ) {
+ // Swapped argument order compared with other messages for backwards compatibility
+ $warning = wfMsgExt( 'fileexists-thumbnail-yes', 'parseinline',
+ $exists['thumbFile']->getTitle()->getPrefixedText(), $filename );
+ } elseif ( $exists['warning'] == 'thumb-name' ) {
+ // Image w/o '180px-' does not exists, but we do not like these filenames
+ $name = $file->getName();
+ $badPart = substr( $name, 0, strpos( $name, '-' ) + 1 );
+ $warning = wfMsgExt( 'file-thumbnail-no', 'parseinline', $badPart );
+ } elseif ( $exists['warning'] == 'bad-prefix' ) {
+ $warning = wfMsgExt( 'filename-bad-prefix', 'parseinline', $exists['prefix'] );
+ } elseif ( $exists['warning'] == 'was-deleted' ) {
# 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>';
+ $llink = $sk->linkKnown(
+ $ltitle,
+ wfMsgHtml( 'deletionlog' ),
+ array(),
+ array(
+ 'type' => 'delete',
+ 'page' => $filename
+ )
+ );
+ $warning = wfMsgWikiHtml( 'filewasdeleted', $llink );
}
+
return $warning;
}
@@ -721,7 +641,7 @@ class UploadForm {
* @param string local filename, e.g. 'file exists', 'non-descriptive filename'
* @return array list of warning messages
*/
- static function ajaxGetExistsWarning( $filename ) {
+ public static function ajaxGetExistsWarning( $filename ) {
$file = wfFindFile( $filename );
if( !$file ) {
// Force local file so we have an object to do further checks against
@@ -730,297 +650,181 @@ class UploadForm {
}
$s = '&nbsp;';
if ( $file ) {
- $warning = self::getExistsWarning( $file );
+ $exists = UploadBase::getExistsWarning( $file );
+ $warning = self::getExistsWarning( $exists );
if ( $warning !== '' ) {
- $s = "<ul>$warning</ul>";
+ $s = "<div>$warning</div>";
}
}
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_FILE, '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.
+ * Construct a warning and a gallery from an array of duplicate files.
*/
- function getDupeWarning( $tempfile, $extension, $destinationTitle ) {
- $hash = File::sha1Base36( $tempfile );
- $dupes = RepoGroup::singleton()->findBySha1( $hash );
- $archivedImage = new ArchivedFile( null, 0, $hash.".$extension" );
+ public static function getDupeWarning( $dupes ) {
if( $dupes ) {
global $wgOut;
$msg = "<gallery>";
foreach( $dupes as $file ) {
$title = $file->getTitle();
- # Don't throw the warning when the titles are the same, it's a reupload
- # and highly redundant.
- if ( !$title->equals( $destinationTitle ) || !$this->mForReUpload ) {
- $msg .= $title->getPrefixedText() .
- "|" . $title->getText() . "\n";
- }
+ $msg .= $title->getPrefixedText() .
+ "|" . $title->getText() . "\n";
}
$msg .= "</gallery>";
return "<li>" .
wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) .
$wgOut->parse( $msg ) .
"</li>\n";
- } elseif ( $archivedImage->getID() > 0 ) {
- global $wgOut;
- $name = Title::makeTitle( NS_FILE, $archivedImage->getName() )->getPrefixedText();
- return Xml::tags( 'li', null, wfMsgExt( 'file-deleted-duplicate', array( 'parseinline' ), array( $name ) ) );
} else {
return '';
}
}
- /**
- * 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;
- }
- }
+/**
+ * Sub class of HTMLForm that provides the form section of SpecialUpload
+ */
+class UploadForm extends HTMLForm {
+ protected $mWatch;
+ protected $mForReUpload;
+ protected $mSessionKey;
+ protected $mHideIgnoreWarning;
+ protected $mDestWarningAck;
+ protected $mDestFile;
+
+ protected $mTextTop;
+ protected $mTextAfterSummary;
+
+ protected $mSourceIds;
- /**
- * 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 );
+ public function __construct( $options = array() ) {
+ global $wgLang;
- if( !$stash ) {
- # Couldn't save the file.
- return false;
- }
+ $this->mWatch = !empty( $options['watch'] );
+ $this->mForReUpload = !empty( $options['forreupload'] );
+ $this->mSessionKey = isset( $options['sessionkey'] )
+ ? $options['sessionkey'] : '';
+ $this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] );
+ $this->mDestWarningAck = !empty( $options['destwarningack'] );
+
+ $this->mTextTop = $options['texttop'];
+ $this->mTextAfterSummary = $options['textaftersummary'];
+ $this->mDestFile = isset( $options['destfile'] ) ? $options['destfile'] : '';
- $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;
- }
+ $sourceDescriptor = $this->getSourceSection();
+ $descriptor = $sourceDescriptor
+ + $this->getDescriptionSection()
+ + $this->getOptionsSection();
- /**
- * Remove a temporarily kept file stashed by saveTempUploadedFile().
- * @access private
- * @return success
- */
- function unsaveUploadedFile() {
- global $wgOut;
- if( !$this->mTempPath ) return true; // nothing to delete
- $repo = RepoGroup::singleton()->getLocalRepo();
- $success = $repo->freeTemp( $this->mTempPath );
- if ( ! $success ) {
- $wgOut->showFileDeleteError( $this->mTempPath );
- return false;
- } else {
- return true;
- }
- }
+ wfRunHooks( 'UploadFormInitDescriptor', array( &$descriptor ) );
+ parent::__construct( $descriptor, 'upload' );
- /* -------------------------------------------------------------- */
+ # Set some form properties
+ $this->setSubmitText( wfMsg( 'uploadbtn' ) );
+ $this->setSubmitName( 'wpUpload' );
+ $this->setSubmitTooltip( 'upload' );
+ $this->setId( 'mw-upload-form' );
+
+ # Build a list of IDs for javascript insertion
+ $this->mSourceIds = array();
+ foreach ( $sourceDescriptor as $key => $field ) {
+ if ( !empty( $field['id'] ) )
+ $this->mSourceIds[] = $field['id'];
+ }
- /**
- * @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
+ * Get the descriptor of the fieldset that contains the file source
+ * selection. The section is 'source'
+ *
+ * @return array Descriptor array
*/
- 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;
+ protected function getSourceSection() {
+ global $wgLang, $wgUser, $wgRequest;
+
+ if ( $this->mSessionKey ) {
+ return array(
+ 'wpSessionKey' => array(
+ 'type' => 'hidden',
+ 'default' => $this->mSessionKey,
+ ),
+ 'wpSourceType' => array(
+ 'type' => 'hidden',
+ 'default' => 'Stash',
+ ),
+ );
}
- $wgOut->addHTML( '<h2>' . wfMsgHtml( 'uploadwarning' ) . "</h2>\n" );
- $wgOut->addHTML( '<ul class="warning">' . $warning . "</ul>\n" );
+ $canUploadByUrl = UploadFromUrl::isEnabled() && $wgUser->isAllowed( 'upload_by_url' );
+ $radio = $canUploadByUrl;
+ $selectedSourceType = strtolower( $wgRequest->getText( 'wpSourceType', 'File' ) );
- $titleObj = SpecialPage::getTitleFor( 'Upload' );
-
- if ( $wgUseCopyrightUpload ) {
- $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" .
- Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n";
- } else {
- $copyright = '';
+ $descriptor = array();
+ if ( $this->mTextTop ) {
+ $descriptor['UploadFormTextTop'] = array(
+ 'type' => 'info',
+ 'section' => 'source',
+ 'default' => $this->mTextTop,
+ 'raw' => true,
+ );
+ }
+
+ $descriptor['UploadFile'] = array(
+ 'class' => 'UploadSourceField',
+ 'section' => 'source',
+ 'type' => 'file',
+ 'id' => 'wpUploadFile',
+ 'label-message' => 'sourcefilename',
+ 'upload-type' => 'File',
+ 'radio' => &$radio,
+ 'help' => wfMsgExt( 'upload-maxfilesize',
+ array( 'parseinline', 'escapenoentities' ),
+ $wgLang->formatSize(
+ wfShorthandToInteger( ini_get( 'upload_max_filesize' ) )
+ )
+ ) . ' ' . wfMsgHtml( 'upload_source_file' ),
+ 'checked' => $selectedSourceType == 'file',
+ );
+ if ( $canUploadByUrl ) {
+ global $wgMaxUploadSize;
+ $descriptor['UploadFileURL'] = array(
+ 'class' => 'UploadSourceField',
+ 'section' => 'source',
+ 'id' => 'wpUploadFileURL',
+ 'label-message' => 'sourceurl',
+ 'upload-type' => 'url',
+ 'radio' => &$radio,
+ 'help' => wfMsgExt( 'upload-maxfilesize',
+ array( 'parseinline', 'escapenoentities' ),
+ $wgLang->formatSize( $wgMaxUploadSize )
+ ) . ' ' . wfMsgHtml( 'upload_source_url' ),
+ 'checked' => $selectedSourceType == 'url',
+ );
}
+ wfRunHooks( 'UploadFormSourceDescriptors', array( &$descriptor, &$radio, $selectedSourceType ) );
- $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"
+ $descriptor['Extensions'] = array(
+ 'type' => 'info',
+ 'section' => 'source',
+ 'default' => $this->getExtensionsMessage(),
+ 'raw' => true,
);
+ return $descriptor;
}
+
/**
- * Displays the main upload form, optionally with a highlighted
- * error message up at the top.
- *
- * @param string $msg as HTML
- * @access private
+ * Get the messages indicating which extensions are preferred and prohibitted.
+ *
+ * @return string HTML string containing the message
*/
- 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\n" );
- return false;
- }
-
- if( $this->mDesiredDestName ) {
- $title = Title::makeTitleSafe( NS_FILE, $this->mDesiredDestName );
- // Show a subtitle link to deleted revisions (to sysops et al only)
- if( $title instanceof Title && ( $count = $title->isDeleted() ) > 0 && $wgUser->isAllowed( 'deletedhistory' ) ) {
- $link = wfMsgExt(
- $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->isDeletedQuick() && !$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" );
-
+ protected function getExtensionsMessage() {
# 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,
+ global $wgLang, $wgCheckFileExtensions, $wgStrictFileExtensions,
$wgFileExtensions, $wgFileBlacklist;
$allowedExtensions = '';
@@ -1045,805 +849,211 @@ wgUploadAutoFill = {$autofill};
# Everything is permitted.
$extensionsList = '';
}
+ return $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' ) );
-
- $msg = $this->mForReUpload ? 'filereuploadsummary' : 'fileuploadsummary';
- $summary = wfMsgExt( $msg, '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"' : '';
- # Re-uploads should not need "file exist already" warnings
- $warningChecked = ($this->mIgnoreWarning || $this->mForReUpload) ? 'checked="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 );
-
+ /**
+ * Get the descriptor of the fieldset that contains the file description
+ * input. The section is 'description'
+ *
+ * @return array Descriptor array
+ */
+ protected function getDescriptionSection() {
+ global $wgUser, $wgOut;
- $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'>"
+ $cols = intval( $wgUser->getOption( 'cols' ) );
+ if( $wgUser->getOption( 'editwidth' ) ) {
+ $wgOut->addInlineStyle( '#mw-htmlform-description { width: 100%; }' );
+ }
+
+ $descriptor = array(
+ 'DestFile' => array(
+ 'type' => 'text',
+ 'section' => 'description',
+ 'id' => 'wpDestFile',
+ 'label-message' => 'destfilename',
+ 'size' => 60,
+ 'default' => $this->mDestFile,
+ # FIXME: hack to work around poor handling of the 'default' option in HTMLForm
+ 'nodata' => strval( $this->mDestFile ) !== '',
+ ),
+ 'UploadDescription' => array(
+ 'type' => 'textarea',
+ 'section' => 'description',
+ 'id' => 'wpUploadDescription',
+ 'label-message' => $this->mForReUpload
+ ? 'filereuploadsummary'
+ : 'fileuploadsummary',
+ 'cols' => $cols,
+ 'rows' => 8,
+ )
);
- if( $this->mForReUpload ) {
- $wgOut->addHTML(
- Xml::hidden( 'wpDestFile', $this->mDesiredDestName, array('id'=>'wpDestFile','tabindex'=>2) ) .
- "<tt>" .
- $encDestName .
- "</tt>"
- );
- }
- else {
- $wgOut->addHTML(
- "<input tabindex='2' type='text' name='wpDestFile' id='wpDestFile' size='60'
- value=\"{$encDestName}\" onchange='toggleFilenameFiller()' $destOnkeyup />"
+ if ( $this->mTextAfterSummary ) {
+ $descriptor['UploadFormTextAfterSummary'] = array(
+ 'type' => 'info',
+ 'section' => 'description',
+ 'default' => $this->mTextAfterSummary,
+ 'raw' => true,
);
}
-
- $wgOut->addHTML(
- "</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>"
+ $descriptor += array(
+ 'EditTools' => array(
+ 'type' => 'edittools',
+ 'section' => 'description',
+ ),
+ 'License' => array(
+ 'type' => 'select',
+ 'class' => 'Licenses',
+ 'section' => 'description',
+ 'id' => 'wpLicense',
+ 'label-message' => 'license',
+ ),
);
- # Re-uploads should not need license info
- if ( !$this->mForReUpload && $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 ( $this->mForReUpload )
+ $descriptor['DestFile']['readonly'] = true;
- if ( !$this->mForReUpload && $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>"
+ global $wgUseCopyrightUpload;
+ if ( $wgUseCopyrightUpload ) {
+ $descriptor['UploadCopyStatus'] = array(
+ 'type' => 'text',
+ 'section' => 'description',
+ 'id' => 'wpUploadCopyStatus',
+ 'label-message' => 'filestatus',
+ );
+ $descriptor['UploadSource'] = array(
+ 'type' => 'text',
+ 'section' => 'description',
+ 'id' => 'wpUploadSource',
+ 'label-message' => 'filesource',
);
}
- $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->addHTML( '<div class="mw-editTools">' );
- $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) );
- $wgOut->addHTML( '</div>' );
- $wgOut->addHTML( "
- </td>
- </tr>" .
- Xml::closeElement( 'table' ) .
- Xml::hidden( 'wpDestFileWarningAck', '', array( 'id' => 'wpDestFileWarningAck' ) ) .
- Xml::hidden( 'wpForReUpload', $this->mForReUpload, array( 'id' => 'wpForReUpload' ) ) .
- 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
- */
- public 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
- */
- public 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) ) {
- if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
- return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) );
- }
-
- # Check IE type
- $fp = fopen( $tmpfile, 'rb' );
- $chunk = fread( $fp, 256 );
- fclose( $fp );
- $extMime = $magic->guessTypesForExtension( $extension );
- $ieTypes = $magic->getIEMimeTypes( $tmpfile, $chunk, $extMime );
- foreach ( $ieTypes as $ieType ) {
- if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
- return new WikiErrorMsg( 'filetype-bad-ie-mime', $ieType );
- }
- }
- }
- }
-
- #check for htmlish code and javascript
- if( $this->detectScript ( $tmpfile, $mime, $extension ) ) {
- return new WikiErrorMsg( 'uploadscripted' );
- }
- if( $extension == 'svg' || $mime == 'image/svg+xml' ) {
- if( $this->detectScriptInSvg( $tmpfile ) ) {
- 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;
+ return $descriptor;
}
/**
- * 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
+ * Get the descriptor of the fieldset that contains the upload options,
+ * such as "watch this file". The section is 'options'
+ *
+ * @return array Descriptor array
*/
- 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 );
- }
+ protected function getOptionsSection() {
+ global $wgUser, $wgOut;
- $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(
- '<a href',
- '<body',
- '<head',
- '<html', #also in safari
- '<img',
- '<pre',
- '<script', #also in safari
- '<table'
+ if( $wgUser->isLoggedIn() ) {
+ $descriptor = array(
+ 'Watchthis' => array(
+ 'type' => 'check',
+ 'id' => 'wpWatchthis',
+ 'label-message' => 'watchthisupload',
+ 'section' => 'options',
+ 'default' => $wgUser->getOption( 'watchcreations' ),
+ )
);
- if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) {
- $tags[] = '<title';
}
-
- foreach( $tags as $tag ) {
- if( false !== strpos( $chunk, $tag ) ) {
- return true;
- }
+ if( !$this->mHideIgnoreWarning ) {
+ $descriptor['IgnoreWarning'] = array(
+ 'type' => 'check',
+ 'id' => 'wpIgnoreWarning',
+ 'label-message' => 'ignorewarnings',
+ 'section' => 'options',
+ );
}
- /*
- * 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;
- }
-
- function detectScriptInSvg( $filename ) {
- $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) );
- return $check->filterMatch;
- }
-
- /**
- * @todo Replace this with a whitelist filter!
- */
- function checkSvgScriptCallback( $element, $attribs ) {
- $stripped = $this->stripXmlNamespace( $element );
-
- if( $stripped == 'script' ) {
- wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" );
- return true;
- }
+ $descriptor['wpDestFileWarningAck'] = array(
+ 'type' => 'hidden',
+ 'id' => 'wpDestFileWarningAck',
+ 'default' => $this->mDestWarningAck ? '1' : '',
+ );
- foreach( $attribs as $attrib => $value ) {
- $stripped = $this->stripXmlNamespace( $attrib );
- if( substr( $stripped, 0, 2 ) == 'on' ) {
- wfDebug( __METHOD__ . ": Found script attribute '$attrib'='value' in uploaded file.\n" );
- return true;
- }
- if( $stripped == 'href' && strpos( strtolower( $value ), 'javascript:' ) !== false ) {
- wfDebug( __METHOD__ . ": Found script href attribute '$attrib'='$value' in uploaded file.\n" );
- return true;
- }
- }
- }
-
- private function stripXmlNamespace( $name ) {
- // 'http://www.w3.org/2000/svg:script' -> 'script'
- $parts = explode( ':', strtolower( $name ) );
- return array_pop( $parts );
- }
-
- /**
- * 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 \n" );
- return $output;
+ if ( $this->mForReUpload ) {
+ $descriptor['wpForReUpload'] = array(
+ 'type' => 'hidden',
+ 'id' => 'wpForReUpload',
+ 'default' => '1',
+ );
}
- }
-
- /**
- * 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();
+ return $descriptor;
- // 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
+ * Add the upload JS and show the form.
*/
- function cleanupTempFile() {
- if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) {
- wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" );
- unlink( $this->mTempPath );
- }
+ public function show() {
+ $this->addUploadJS();
+ parent::show();
}
/**
- * 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
+ * Add upload JS to $wgOut
+ *
+ * @param bool $autofill Whether or not to autofill the destination
+ * filename text box
*/
- public static function userCanReUpload( User $user, $img ) {
- if( $user->isAllowed( 'reupload' ) )
- return true; // non-conditional
- if( !$user->isAllowed( 'reupload-own' ) )
- return false;
+ protected function addUploadJS( ) {
+ global $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview, $wgEnableAPI;
+ global $wgOut;
- $dbr = wfGetDB( DB_SLAVE );
- $row = $dbr->selectRow('image',
- /* SELECT */ 'img_user',
- /* WHERE */ array( 'img_name' => $img )
+ $useAjaxDestCheck = $wgUseAjax && $wgAjaxUploadDestCheck;
+ $useAjaxLicensePreview = $wgUseAjax && $wgAjaxLicensePreview && $wgEnableAPI;
+
+ $scriptVars = array(
+ 'wgAjaxUploadDestCheck' => $useAjaxDestCheck,
+ 'wgAjaxLicensePreview' => $useAjaxLicensePreview,
+ 'wgUploadAutoFill' => !$this->mForReUpload &&
+ // If we received mDestFile from the request, don't autofill
+ // the wpDestFile textbox
+ $this->mDestFile === '',
+ 'wgUploadSourceIds' => $this->mSourceIds,
);
- if ( !$row )
- return false;
- return $user->getId() == $row->img_user;
+ $wgOut->addScript( Skin::makeVariablesScript( $scriptVars ) );
+
+ // For <charinsert> support
+ $wgOut->addScriptFile( 'edit.js' );
+ $wgOut->addScriptFile( 'upload.js' );
}
/**
- * Display an error with a wikitext description
+ * Empty function; submission is handled elsewhere.
+ *
+ * @return bool false
*/
- function showError( $description ) {
- global $wgOut;
- $wgOut->setPageTitle( wfMsg( "internalerror" ) );
- $wgOut->setRobotPolicy( "noindex,nofollow" );
- $wgOut->setArticleRelated( false );
- $wgOut->enableClientCache( false );
- $wgOut->addWikiText( $description );
+ function trySubmit() {
+ return false;
}
- /**
- * 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 class="mw-warning-with-logexcerpt">' );
- $out->addWikiMsg( 'upload-wasdeleted' );
- $out->addHTML(
- $loglist->beginLogEventsList() .
- $pager->getBody() .
- $loglist->endLogEventsList()
+/**
+ * A form field that contains a radio box in the label
+ */
+class UploadSourceField extends HTMLTextField {
+ function getLabelHtml() {
+ $id = "wpSourceType{$this->mParams['upload-type']}";
+ $label = Html::rawElement( 'label', array( 'for' => $id ), $this->mLabel );
+
+ if ( !empty( $this->mParams['radio'] ) ) {
+ $attribs = array(
+ 'name' => 'wpSourceType',
+ 'type' => 'radio',
+ 'id' => $id,
+ 'value' => $this->mParams['upload-type'],
);
- $out->addHTML( '</div>' );
+ if ( !empty( $this->mParams['checked'] ) )
+ $attribs['checked'] = 'checked';
+ $label .= Html::element( 'input', $attribs );
}
+
+ return Html::rawElement( 'td', array( 'class' => 'mw-label' ), $label );
+ }
+ function getSize() {
+ return isset( $this->mParams['size'] )
+ ? $this->mParams['size']
+ : 60;
}
}
+
diff --git a/includes/specials/SpecialUploadMogile.php b/includes/specials/SpecialUploadMogile.php
deleted file mode 100644
index 7ff8fda6..00000000
--- a/includes/specials/SpecialUploadMogile.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?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
index 8616ae28..8b8d0e9e 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -35,21 +35,23 @@ class LoginForm {
const CREATE_BLOCKED = 9;
const THROTTLED = 10;
const USER_BLOCKED = 11;
- const NEED_TOKEN = 12;
- const WRONG_TOKEN = 13;
+ const NEED_TOKEN = 12;
+ const WRONG_TOKEN = 13;
var $mName, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted;
var $mAction, $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
- var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage, $mSkipCookieCheck;
- var $mToken;
+ var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
+ var $mSkipCookieCheck, $mReturnToQuery, $mToken;
+
+ private $mExtUser = null;
/**
* Constructor
- * @param WebRequest $request A WebRequest object passed by reference
+ * @param $request WebRequest: a WebRequest object passed by reference
+ * @param $par String: subpage parameter
*/
function LoginForm( &$request, $par = '' ) {
- global $wgLang, $wgAllowRealName, $wgEnableEmail;
- global $wgAuth, $wgRedirectOnLogin;
+ global $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
$this->mType = ( $par == 'signup' ) ? $par : $request->getText( 'type' ); # Check for [[Special:Userlogin/signup]]
$this->mName = $request->getText( 'wpName' );
@@ -57,6 +59,7 @@ class LoginForm {
$this->mRetype = $request->getText( 'wpRetype' );
$this->mDomain = $request->getText( 'wpDomain' );
$this->mReturnTo = $request->getVal( 'returnto' );
+ $this->mReturnToQuery = $request->getVal( 'returntoquery' );
$this->mCookieCheck = $request->getVal( 'wpCookieCheck' );
$this->mPosted = $request->wasPosted();
$this->mCreateaccount = $request->getCheck( 'wpCreateaccount' );
@@ -73,6 +76,7 @@ class LoginForm {
if ( $wgRedirectOnLogin ) {
$this->mReturnTo = $wgRedirectOnLogin;
+ $this->mReturnToQuery = '';
}
if( $wgEnableEmail ) {
@@ -80,7 +84,7 @@ class LoginForm {
} else {
$this->mEmail = '';
}
- if( $wgAllowRealName ) {
+ if( !in_array( 'realname', $wgHiddenPrefs ) ) {
$this->mRealName = $request->getText( 'wpRealName' );
} else {
$this->mRealName = '';
@@ -92,8 +96,10 @@ class LoginForm {
$wgAuth->setDomain( $this->mDomain );
# When switching accounts, it sucks to get automatically logged out
- if( $this->mReturnTo == $wgLang->specialPage( 'Userlogout' ) ) {
+ $returnToTitle = Title::newFromText( $this->mReturnTo );
+ if( is_object( $returnToTitle ) && $returnToTitle->isSpecial( 'Userlogout' ) ) {
$this->mReturnTo = '';
+ $this->mReturnToQuery = '';
}
}
@@ -121,14 +127,14 @@ class LoginForm {
function addNewAccountMailPassword() {
global $wgOut;
- if ('' == $this->mEmail) {
+ if ( $this->mEmail == '' ) {
$this->mainLoginForm( wfMsg( 'noemail', htmlspecialchars( $this->mName ) ) );
return;
}
$u = $this->addNewaccountInternal();
- if ($u == NULL) {
+ if ($u == null) {
return;
}
@@ -162,7 +168,7 @@ class LoginForm {
# Create the account and abort if there's a problem doing so
$u = $this->addNewAccountInternal();
- if( $u == NULL )
+ if( $u == null )
return;
# If we showed up language selection links, and one was in use, be
@@ -191,7 +197,7 @@ class LoginForm {
if( $wgUser->isAnon() ) {
$wgUser = $u;
$wgUser->setCookies();
- wfRunHooks( 'AddNewAccount', array( $wgUser ) );
+ wfRunHooks( 'AddNewAccount', array( $wgUser, false ) );
$wgUser->addNewUserLogEntry();
if( $this->hasSessionCookie() ) {
return $this->successfulCreation();
@@ -207,7 +213,7 @@ class LoginForm {
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->addHTML( wfMsgWikiHtml( 'accountcreatedtext', $u->getName() ) );
$wgOut->returnToMain( false, $self );
- wfRunHooks( 'AddNewAccount', array( $u ) );
+ wfRunHooks( 'AddNewAccount', array( $u, false ) );
$u->addNewUserLogEntry();
return true;
}
@@ -218,7 +224,6 @@ class LoginForm {
*/
function addNewAccountInternal() {
global $wgUser, $wgOut;
- global $wgEnableSorbs, $wgProxyWhitelist;
global $wgMemc, $wgAccountCreationThrottle;
global $wgAuth, $wgMinimalPasswordLength;
global $wgEmailConfirmToEdit;
@@ -234,7 +239,7 @@ class LoginForm {
// cation server before they create an account (otherwise, they can
// create a local account and login as any domain user). We only need
// to check this for domains that aren't local.
- if( 'local' != $this->mDomain && '' != $this->mDomain ) {
+ if( 'local' != $this->mDomain && $this->mDomain != '' ) {
if( !$wgAuth->canCreateAccounts() && ( !$wgAuth->userExists( $this->mName ) || !$wgAuth->authenticate( $this->mName, $this->mPassword ) ) ) {
$this->mainLoginForm( wfMsg( 'wrongpassword' ) );
return false;
@@ -275,9 +280,7 @@ class LoginForm {
}
$ip = wfGetIP();
- if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
- $wgUser->inSorbsBlacklist( $ip ) )
- {
+ if ( $wgUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
$this->mainLoginForm( wfMsg( 'sorbs_create_account_reason' ) . ' (' . htmlspecialchars( $ip ) . ')' );
return false;
}
@@ -285,7 +288,7 @@ class LoginForm {
# 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 ) ) {
+ if ( !is_object( $u ) ) {
$this->mainLoginForm( wfMsg( 'noname' ) );
return false;
}
@@ -301,9 +304,10 @@ class LoginForm {
}
# check for minimal password length
- if ( !$u->isValidPassword( $this->mPassword ) ) {
+ $valid = $u->getPasswordValidity( $this->mPassword );
+ if ( $valid !== true ) {
if ( !$this->mCreateaccountMail ) {
- $this->mainLoginForm( wfMsgExt( 'passwordtooshort', array( 'parsemag' ), $wgMinimalPasswordLength ) );
+ $this->mainLoginForm( wfMsgExt( $valid, array( 'parsemag' ), $wgMinimalPasswordLength ) );
return false;
} else {
# do not force a password for account creation by email
@@ -383,6 +387,14 @@ class LoginForm {
$wgAuth->initUser( $u, $autocreate );
+ if ( $this->mExtUser ) {
+ $this->mExtUser->linkToLocal( $u->getId() );
+ $email = $this->mExtUser->getPref( 'emailaddress' );
+ if ( $email && !$this->mEmail ) {
+ $u->setEmail( $email );
+ }
+ }
+
$u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
$u->saveSettings();
@@ -399,15 +411,13 @@ class LoginForm {
* This may create a local account as a side effect if the
* authentication plugin allows transparent local account
* creation.
- *
- * @public
*/
- function authenticateUserData() {
+ public function authenticateUserData() {
global $wgUser, $wgAuth;
- if ( '' == $this->mName ) {
+ if ( $this->mName == '' ) {
return self::NO_NAME;
}
-
+
// We require a login token to prevent login CSRF
// Handle part of this before incrementing the throttle so
// token-less login attempts don't count towards the throttle
@@ -422,17 +432,17 @@ class LoginForm {
if ( !$this->mToken ) {
return self::NEED_TOKEN;
}
-
+
global $wgPasswordAttemptThrottle;
- $throttleCount=0;
- if ( is_array($wgPasswordAttemptThrottle) ) {
+ $throttleCount = 0;
+ if ( is_array( $wgPasswordAttemptThrottle ) ) {
$throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
$count = $wgPasswordAttemptThrottle['count'];
$period = $wgPasswordAttemptThrottle['seconds'];
global $wgMemc;
- $throttleCount = $wgMemc->get($throttleKey);
+ $throttleCount = $wgMemc->get( $throttleKey );
if ( !$throttleCount ) {
$wgMemc->add( $throttleKey, 1, $period ); // start counter
} else if ( $throttleCount < $count ) {
@@ -457,8 +467,13 @@ class LoginForm {
wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
return self::SUCCESS;
}
+
+ $this->mExtUser = ExternalUser::newFromName( $this->mName );
+
+ # TODO: Allow some magic here for invalid external names, e.g., let the
+ # user choose a different wiki name.
$u = User::newFromName( $this->mName );
- if( is_null( $u ) || !User::isUsableName( $u->getName() ) ) {
+ if( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) {
return self::ILLEGAL;
}
@@ -471,6 +486,15 @@ class LoginForm {
$isAutoCreated = true;
}
} else {
+ global $wgExternalAuthType, $wgAutocreatePolicy;
+ if ( $wgExternalAuthType && $wgAutocreatePolicy != 'never'
+ && is_object( $this->mExtUser )
+ && $this->mExtUser->authenticate( $this->mPassword ) ) {
+ # The external user and local user have the same name and
+ # password, so we assume they're the same.
+ $this->mExtUser->linkToLocal( $u->getID() );
+ }
+
$u->load();
}
@@ -480,6 +504,7 @@ class LoginForm {
return $abort;
}
+ global $wgBlockDisablesLogin;
if (!$u->checkPassword( $this->mPassword )) {
if( $u->checkTemporaryPassword( $this->mPassword ) ) {
// The e-mailed temporary password should not be used for actu-
@@ -508,8 +533,11 @@ class LoginForm {
// faces etc will probably just fail cleanly here.
$retval = self::RESET_PASS;
} else {
- $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
+ $retval = ($this->mPassword == '') ? self::EMPTY_PASS : self::WRONG_PASS;
}
+ } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) {
+ // If we've enabled it, make it so that a blocked user cannot login
+ $retval = self::USER_BLOCKED;
} else {
$wgAuth->updateUser( $u );
$wgUser = $u;
@@ -536,26 +564,40 @@ class LoginForm {
* @return integer Status code
*/
function attemptAutoCreate( $user ) {
- global $wgAuth, $wgUser;
+ global $wgAuth, $wgUser, $wgAutocreatePolicy;
+
+ if ( $wgUser->isBlockedFromCreateAccount() ) {
+ wfDebug( __METHOD__.": user is blocked from account creation\n" );
+ return self::CREATE_BLOCKED;
+ }
+
/**
* If the external authentication plugin allows it, automatically cre-
* ate a new account for users that are externally defined but have not
* yet logged in.
*/
- if ( !$wgAuth->autoCreate() ) {
- return self::NOT_EXISTS;
- }
- 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;
+ if ( $this->mExtUser ) {
+ # mExtUser is neither null nor false, so use the new ExternalAuth
+ # system.
+ if ( $wgAutocreatePolicy == 'never' ) {
+ return self::NOT_EXISTS;
+ }
+ if ( !$this->mExtUser->authenticate( $this->mPassword ) ) {
+ return self::WRONG_PLUGIN_PASS;
+ }
+ } else {
+ # Old AuthPlugin.
+ 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;
+ }
}
wfDebug( __METHOD__.": creating account\n" );
@@ -566,8 +608,7 @@ class LoginForm {
function processLogin() {
global $wgUser, $wgAuth;
- switch ($this->authenticateUserData())
- {
+ switch ( $this->authenticateUserData() ) {
case self::SUCCESS:
# We've verified now, update the real record
if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
@@ -630,6 +671,10 @@ class LoginForm {
case self::THROTTLED:
$this->mainLoginForm( wfMsg( 'login-throttled' ) );
break;
+ case self::USER_BLOCKED:
+ $this->mainLoginForm( wfMsgExt( 'login-userblocked',
+ array( 'parsemag', 'escape' ), $this->mName ) );
+ break;
default:
throw new MWException( "Unhandled case value" );
}
@@ -664,6 +709,13 @@ class LoginForm {
$this->mainLoginForm( wfMsg( 'blocked-mailpassword' ) );
return;
}
+
+ # Check for hooks
+ $error = null;
+ if ( ! wfRunHooks( 'UserLoginMailPassword', array( $this->mName, &$error ) ) ) {
+ $this->mainLoginForm( $error );
+ return;
+ }
# If the user doesn't have a login token yet, set one.
if ( !self::getLoginToken() ) {
@@ -684,12 +736,12 @@ class LoginForm {
return;
}
- if ( '' == $this->mName ) {
+ if ( $this->mName == '' ) {
$this->mainLoginForm( wfMsg( 'noname' ) );
return;
}
$u = User::newFromName( $this->mName );
- if( is_null( $u ) ) {
+ if( !$u instanceof User ) {
$this->mainLoginForm( wfMsg( 'noname' ) );
return;
}
@@ -725,17 +777,17 @@ class LoginForm {
/**
- * @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
+ * @param $u User object
+ * @param $throttle Boolean
+ * @param $emailTitle String: message name of email title
+ * @param $emailText 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 $wgServer, $wgScript, $wgUser, $wgNewPasswordExpiry;
- if ( '' == $u->getEmail() ) {
+ if ( $u->getEmail() == '' ) {
return new WikiError( wfMsg( 'noemail', $u->getName() ) );
}
$ip = wfGetIP();
@@ -748,10 +800,10 @@ class LoginForm {
$np = $u->randomPassword();
$u->setNewpassword( $np, $throttle );
$u->saveSettings();
-
- $m = wfMsgExt( $emailText, array( 'parsemag' ), $ip, $u->getName(), $np,
+ $userLanguage = $u->getOption( 'language' );
+ $m = wfMsgExt( $emailText, array( 'parsemag', 'language' => $userLanguage ), $ip, $u->getName(), $np,
$wgServer . $wgScript, round( $wgNewPasswordExpiry / 86400 ) );
- $result = $u->sendMail( wfMsg( $emailTitle ), $m );
+ $result = $u->sendMail( wfMsgExt( $emailTitle, array( 'parsemag', 'language' => $userLanguage ) ), $m );
return $result;
}
@@ -781,8 +833,7 @@ class LoginForm {
if ( !$titleObj instanceof Title ) {
$titleObj = Title::newMainPage();
}
-
- $wgOut->redirect( $titleObj->getFullURL() );
+ $wgOut->redirect( $titleObj->getFullURL( $this->mReturnToQuery ) );
}
}
@@ -815,7 +866,7 @@ class LoginForm {
$wgOut->addHTML( $injected_html );
if ( !empty( $this->mReturnTo ) ) {
- $wgOut->returnToMain( null, $this->mReturnTo );
+ $wgOut->returnToMain( null, $this->mReturnTo, $this->mReturnToQuery );
} else {
$wgOut->returnToMain( null );
}
@@ -868,7 +919,7 @@ class LoginForm {
* @private
*/
function mainLoginForm( $msg, $msgtype = 'error' ) {
- global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail;
+ global $wgUser, $wgOut, $wgHiddenPrefs, $wgEnableEmail;
global $wgCookiePrefix, $wgLoginLanguageSelector;
global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration;
@@ -890,7 +941,7 @@ class LoginForm {
}
}
- if ( '' == $this->mName ) {
+ if ( $this->mName == '' ) {
if ( $wgUser->isLoggedIn() ) {
$this->mName = $wgUser->getName();
} else {
@@ -914,6 +965,9 @@ class LoginForm {
if ( !empty( $this->mReturnTo ) ) {
$returnto = '&returnto=' . wfUrlencode( $this->mReturnTo );
+ if ( !empty( $this->mReturnToQuery ) )
+ $returnto .= '&returntoquery=' .
+ wfUrlencode( $this->mReturnToQuery );
$q .= $returnto;
$linkq .= $returnto;
}
@@ -928,7 +982,7 @@ class LoginForm {
# Don't show a "create account" link if the user can't
if( $this->showCreateOrLoginLink( $wgUser ) )
- $template->set( 'link', wfMsgHtml( $linkmsg, $link ) );
+ $template->set( 'link', wfMsgWikiHtml( $linkmsg, $link ) );
else
$template->set( 'link', '' );
@@ -944,7 +998,7 @@ class LoginForm {
$template->set( 'message', $msg );
$template->set( 'messagetype', $msgtype );
$template->set( 'createemail', $wgEnableEmail && $wgUser->isLoggedIn() );
- $template->set( 'userealname', $wgAllowRealName );
+ $template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs ) );
$template->set( 'useemail', $wgEnableEmail );
$template->set( 'emailrequired', $wgEmailConfirmToEdit );
$template->set( 'canreset', $wgAuth->allowPasswordChange() );
@@ -971,14 +1025,20 @@ class LoginForm {
}
// Give authentication and captcha plugins a chance to modify the form
- $wgAuth->modifyUITemplate( $template );
+ $wgAuth->modifyUITemplate( $template, $this->mType );
if ( $this->mType == 'signup' ) {
wfRunHooks( 'UserCreateForm', array( &$template ) );
} else {
wfRunHooks( 'UserLoginForm', array( &$template ) );
}
- $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
+ //Changes the title depending on permissions for creating account
+ if ( $wgUser->isAllowed( 'createaccount' ) ) {
+ $wgOut->setPageTitle( wfMsg( 'userlogin' ) );
+ } else {
+ $wgOut->setPageTitle( wfMsg( 'userloginnocreate' ) );
+ }
+
$wgOut->setRobotPolicy( 'noindex,nofollow' );
$wgOut->setArticleRelated( false );
$wgOut->disallowUserJs(); // just in case...
@@ -1080,8 +1140,6 @@ class LoginForm {
* @private
*/
function onCookieRedirectCheck( $type ) {
- global $wgUser;
-
if ( !$this->hasSessionCookie() ) {
if ( $type == 'new' ) {
return $this->mainLoginForm( wfMsgExt( 'nocookiesnew', array( 'parseinline' ) ) );
@@ -1139,12 +1197,17 @@ class LoginForm {
function makeLanguageSelectorLink( $text, $lang ) {
global $wgUser;
$self = SpecialPage::getTitleFor( 'Userlogin' );
- $attr[] = 'uselang=' . $lang;
+ $attr = array( 'uselang' => $lang );
if( $this->mType == 'signup' )
- $attr[] = 'type=signup';
+ $attr['type'] = 'signup';
if( $this->mReturnTo )
- $attr[] = 'returnto=' . $this->mReturnTo;
+ $attr['returnto'] = $this->mReturnTo;
$skin = $wgUser->getSkin();
- return $skin->makeKnownLinkObj( $self, htmlspecialchars( $text ), implode( '&', $attr ) );
+ return $skin->linkKnown(
+ $self,
+ htmlspecialchars( $text ),
+ array(),
+ $attr
+ );
}
}
diff --git a/includes/specials/SpecialUserlogout.php b/includes/specials/SpecialUserlogout.php
index 3d497bd7..e23df612 100644
--- a/includes/specials/SpecialUserlogout.php
+++ b/includes/specials/SpecialUserlogout.php
@@ -10,6 +10,16 @@
function wfSpecialUserlogout() {
global $wgUser, $wgOut;
+ /**
+ * Some satellite ISPs use broken precaching schemes that log people out straight after
+ * they're logged in (bug 17790). Luckily, there's a way to detect such requests.
+ */
+ if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( $_SERVER['REQUEST_URI'], '&amp;' ) !== false ) {
+ wfDebug( "Special:Userlogout request {$_SERVER['REQUEST_URI']} looks suspicious, denying.\n" );
+ wfHttpError( 400, wfMsg( 'loginerror' ), wfMsg( 'suspicious-userlogout' ) );
+ return;
+ }
+
$oldName = $wgUser->getName();
$wgUser->logout();
$wgOut->setRobotPolicy( 'noindex,nofollow' );
diff --git a/includes/specials/SpecialUserrights.php b/includes/specials/SpecialUserrights.php
index 90619109..36caf9a6 100644
--- a/includes/specials/SpecialUserrights.php
+++ b/includes/specials/SpecialUserrights.php
@@ -34,8 +34,8 @@ class UserrightsPage extends SpecialPage {
return !empty( $available['add'] )
or !empty( $available['remove'] )
or ( ( $this->isself || !$checkIfSelf ) and
- (!empty( $available['add-self'] )
- or !empty( $available['remove-self'] )));
+ ( !empty( $available['add-self'] )
+ or !empty( $available['remove-self'] ) ) );
}
/**
@@ -44,10 +44,10 @@ class UserrightsPage extends SpecialPage {
*
* @param $par Mixed: string if any subpage provided, else null
*/
- function execute( $par ) {
+ public 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;
+ global $wgUser, $wgRequest, $wgOut;
if( $par ) {
$this->mTarget = $par;
@@ -55,32 +55,41 @@ class UserrightsPage extends SpecialPage {
$this->mTarget = $wgRequest->getVal( 'user' );
}
- if (!$this->mTarget) {
+ /*
+ * If the user is blocked and they only have "partial" access
+ * (e.g. they don't have the userrights permission), then don't
+ * allow them to use Special:UserRights.
+ */
+ if( $wgUser->isBlocked() && !$wgUser->isAllowed( 'userrights' ) ) {
+ $wgOut->blockedPage();
+ return;
+ }
+
+ $available = $this->changeableGroups();
+
+ 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']))
+ if ( !count( $available['add'] ) && !count( $available['remove'] ) )
$this->mTarget = $wgUser->getName();
}
- if ($this->mTarget == $wgUser->getName())
+ if ( $this->mTarget == $wgUser->getName() )
$this->isself = true;
if( !$this->userCanChangeRights( $wgUser, true ) ) {
// fixme... there may be intermediate groups we can mention.
- global $wgOut;
- $wgOut->showPermissionsErrorPage( array(
+ $wgOut->showPermissionsErrorPage( array( array(
$wgUser->isAnon()
? 'userrights-nologin'
- : 'userrights-notallowed' ) );
+ : 'userrights-notallowed' ) ) );
return;
}
if ( wfReadOnly() ) {
- global $wgOut;
$wgOut->readOnlyPage();
return;
}
@@ -90,7 +99,8 @@ class UserrightsPage extends SpecialPage {
$this->setHeaders();
// show the general form
- $this->switchForm();
+ if ( count( $available['add'] ) || count( $available['remove'] ) )
+ $this->switchForm();
if( $wgRequest->wasPosted() ) {
// save settings
@@ -102,9 +112,7 @@ class UserrightsPage extends SpecialPage {
$this->mTarget,
$reason
);
-
- global $wgOut;
-
+
$url = $this->getSuccessURL();
$wgOut->redirect( $url );
return;
@@ -117,7 +125,7 @@ class UserrightsPage extends SpecialPage {
$this->editUserGroupsForm( $this->mTarget );
}
}
-
+
function getSuccessURL() {
return $this->getTitle( $this->mTarget )->getFullURL();
}
@@ -130,11 +138,12 @@ class UserrightsPage extends SpecialPage {
* @param $reason String: reason for group change
* @return null
*/
- function saveUserGroups( $username, $reason = '') {
+ function saveUserGroups( $username, $reason = '' ) {
global $wgRequest, $wgUser, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
$user = $this->fetchUser( $username );
- if( !$user ) {
+ if( $user instanceof WikiErrorMsg ) {
+ $wgOut->addWikiMsgArray( $user->getMessageKey(), $user->getMessageArgs() );
return;
}
@@ -144,38 +153,58 @@ class UserrightsPage extends SpecialPage {
// 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) {
+ 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" )) {
+ if ( $wgRequest->getCheck( "wpGroup-$group" ) ) {
$addgroup[] = $group;
} else {
$removegroup[] = $group;
}
}
+
+ $this->doSaveUserGroups( $user, $addgroup, $removegroup, $reason );
+ }
+
+ /**
+ * Save user groups changes in the database.
+ *
+ * @param $user User object
+ * @param $add Array of groups to add
+ * @param $remove Array of groups to remove
+ * @param $reason String: reason for group change
+ * @return Array: Tuple of added, then removed groups
+ */
+ function doSaveUserGroups( $user, $add, $remove, $reason = '' ) {
+ global $wgUser;
// Validate input set...
+ $isself = ( $user->getName() == $wgUser->getName() );
+ $groups = $user->getGroups();
$changeable = $this->changeableGroups();
- $addable = array_merge( $changeable['add'], $this->isself ? $changeable['add-self'] : array() );
- $removable = array_merge( $changeable['remove'], $this->isself ? $changeable['remove-self'] : array() );
-
- $removegroup = array_unique(
- array_intersect( (array)$removegroup, $removable ) );
- $addgroup = array_unique(
- array_intersect( (array)$addgroup, $addable ) );
+ $addable = array_merge( $changeable['add'], $isself ? $changeable['add-self'] : array() );
+ $removable = array_merge( $changeable['remove'], $isself ? $changeable['remove-self'] : array() );
+
+ $remove = array_unique(
+ array_intersect( (array)$remove, $removable, $groups ) );
+ $add = array_unique( array_diff(
+ array_intersect( (array)$add, $addable ),
+ $groups )
+ );
$oldGroups = $user->getGroups();
$newGroups = $oldGroups;
+
// remove then add groups
- if( $removegroup ) {
- $newGroups = array_diff($newGroups, $removegroup);
- foreach( $removegroup as $group ) {
+ if( $remove ) {
+ $newGroups = array_diff( $newGroups, $remove );
+ foreach( $remove as $group ) {
$user->removeGroup( $group );
}
}
- if( $addgroup ) {
- $newGroups = array_merge($newGroups, $addgroup);
- foreach( $addgroup as $group ) {
+ if( $add ) {
+ $newGroups = array_merge( $newGroups, $add );
+ foreach( $add as $group ) {
$user->addGroup( $group );
}
}
@@ -186,26 +215,24 @@ class UserrightsPage extends SpecialPage {
wfDebug( 'oldGroups: ' . print_r( $oldGroups, true ) );
wfDebug( 'newGroups: ' . print_r( $newGroups, true ) );
- if( $user instanceof User ) {
- // hmmm
- wfRunHooks( 'UserRights', array( &$user, $addgroup, $removegroup ) );
- }
+ wfRunHooks( 'UserRights', array( &$user, $add, $remove ) );
if( $newGroups != $oldGroups ) {
- $this->addLogEntry( $user, $oldGroups, $newGroups );
+ $this->addLogEntry( $user, $oldGroups, $newGroups, $reason );
}
+ return array( $add, $remove );
}
-
+
+
/**
* Add a rights log entry for an action.
*/
- function addLogEntry( $user, $oldGroups, $newGroups ) {
- global $wgRequest;
+ function addLogEntry( $user, $oldGroups, $newGroups, $reason ) {
$log = new LogPage( 'rights' );
$log->addEntry( 'rights',
$user->getUserPage(),
- $wgRequest->getText( 'user-reason' ),
+ $reason,
array(
$this->makeGroupNameListForLog( $oldGroups ),
$this->makeGroupNameListForLog( $newGroups )
@@ -221,7 +248,8 @@ class UserrightsPage extends SpecialPage {
global $wgOut;
$user = $this->fetchUser( $username );
- if( !$user ) {
+ if( $user instanceof WikiErrorMsg ) {
+ $wgOut->addWikiMsgArray( $user->getMessageKey(), $user->getMessageArgs() );
return;
}
@@ -239,10 +267,10 @@ class UserrightsPage extends SpecialPage {
* return a user (or proxy) object for manipulating it.
*
* Side effects: error output for invalid access
- * @return mixed User, UserRightsProxy, or null
+ * @return mixed User, UserRightsProxy, or WikiErrorMsg
*/
- function fetchUser( $username ) {
- global $wgOut, $wgUser, $wgUserrightsInterwikiDelimiter;
+ public function fetchUser( $username ) {
+ global $wgUser, $wgUserrightsInterwikiDelimiter;
$parts = explode( $wgUserrightsInterwikiDelimiter, $username );
if( count( $parts ) < 2 ) {
@@ -250,20 +278,21 @@ class UserrightsPage extends SpecialPage {
$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( $database == wfWikiID() ) {
+ $database = '';
+ } else {
+ if( !$wgUser->isAllowed( 'userrights-interwiki' ) ) {
+ return new WikiErrorMsg( 'userrights-no-interwiki' );
+ }
+ if( !UserRightsProxy::validDatabase( $database ) ) {
+ return new WikiErrorMsg( 'userrights-nodatabase', $database );
+ }
}
}
if( $name == '' ) {
- $wgOut->addWikiMsg( 'nouserspecified' );
- return false;
+ return new WikiErrorMsg( 'nouserspecified' );
}
if( $name{0} == '#' ) {
@@ -278,8 +307,13 @@ class UserrightsPage extends SpecialPage {
}
if( !$name ) {
- $wgOut->addWikiMsg( 'noname' );
- return null;
+ return new WikiErrorMsg( 'noname' );
+ }
+ } else {
+ $name = User::getCanonicalName( $name );
+ if( !$name ) {
+ // invalid name
+ return new WikiErrorMsg( 'nosuchusershort', $username );
}
}
@@ -290,8 +324,7 @@ class UserrightsPage extends SpecialPage {
}
if( !$user || $user->isAnon() ) {
- $wgOut->addWikiMsg( 'nosuchusershort', $username );
- return null;
+ return new WikiErrorMsg( 'nosuchusershort', $username );
}
return $user;
@@ -339,14 +372,16 @@ class UserrightsPage extends SpecialPage {
* @return Array: Tuple of addable, then removable groups
*/
protected function splitGroups( $groups ) {
- list($addable, $removable, $addself, $removeself) = array_values( $this->changeableGroups() );
+ list( $addable, $removable, $addself, $removeself ) = array_values( $this->changeableGroups() );
$removable = array_intersect(
- array_merge( $this->isself ? $removeself : array(), $removable ),
- $groups ); // Can't remove groups the user doesn't have
- $addable = array_diff(
- array_merge( $this->isself ? $addself : array(), $addable ),
- $groups ); // Can't add groups the user does have
+ array_merge( $this->isself ? $removeself : array(), $removable ),
+ $groups
+ ); // Can't remove groups the user doesn't have
+ $addable = array_diff(
+ array_merge( $this->isself ? $addself : array(), $addable ),
+ $groups
+ ); // Can't add groups the user does have
return array( $addable, $removable );
}
@@ -364,10 +399,21 @@ class UserrightsPage extends SpecialPage {
foreach( $groups as $group )
$list[] = self::buildGroupLink( $group );
+ $autolist = array();
+ if ( $user instanceof User ) {
+ foreach( Autopromote::getAutopromoteGroups( $user ) as $group ) {
+ $autolist[] = self::buildGroupLink( $group );
+ }
+ }
+
$grouplist = '';
if( count( $list ) > 0 ) {
$grouplist = wfMsgHtml( 'userrights-groupsmember' );
- $grouplist = '<p>' . $grouplist . ' ' . $wgLang->listToText( $list ) . '</p>';
+ $grouplist = '<p>' . $grouplist . ' ' . $wgLang->listToText( $list ) . "</p>\n";
+ }
+ if( count( $autolist ) > 0 ) {
+ $autogrouplistintro = wfMsgHtml( 'userrights-groupsmember-auto' );
+ $grouplist .= '<p>' . $autogrouplistintro . ' ' . $wgLang->listToText( $autolist ) . "</p>\n";
}
$wgOut->addHTML(
Xml::openElement( 'form', array( 'method' => 'post', 'action' => $this->getTitle()->getLocalURL(), 'name' => 'editGroup', 'id' => 'mw-userrights-form2' ) ) .
@@ -409,17 +455,17 @@ class UserrightsPage extends SpecialPage {
private static function buildGroupLink( $group ) {
static $cache = array();
if( !isset( $cache[$group] ) )
- $cache[$group] = User::makeGroupLinkHtml( $group, User::getGroupName( $group ) );
+ $cache[$group] = User::makeGroupLinkHtml( $group, htmlspecialchars( 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();
- }
+ protected static function getAllGroups() {
+ return User::getAllGroups();
+ }
/**
* Adds a table with checkboxes where you can select what groups to add/remove
@@ -431,11 +477,11 @@ class UserrightsPage extends SpecialPage {
$allgroups = $this->getAllGroups();
$ret = '';
- $column = 1;
- $settable_col = '';
- $unsettable_col = '';
+ # Put all column info into an associative array so that extensions can
+ # more easily manage it.
+ $columns = array( 'unchangeable' => array(), 'changeable' => array() );
- foreach ($allgroups as $group) {
+ foreach( $allgroups as $group ) {
$set = in_array( $group, $usergroups );
# Should the checkbox be disabled?
$disabled = !(
@@ -443,53 +489,54 @@ class UserrightsPage extends SpecialPage {
( !$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";
+ ( $set && !$this->canAdd( $group ) ) ||
+ ( !$set && !$this->canRemove( $group ) ) );
+
+ $checkbox = array(
+ 'set' => $set,
+ 'disabled' => $disabled,
+ 'irreversible' => $irreversible
+ );
+
+ if( $disabled ) {
+ $columns['unchangeable'][$group] = $checkbox;
} else {
- $settable_col .= "$checkbox<br />\n";
+ $columns['changeable'][$group] = $checkbox;
}
}
- 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>
-";
+ # Build the HTML table
+ $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) .
+ "<tr>\n";
+ foreach( $columns as $name => $column ) {
+ if( $column === array() )
+ continue;
+ $ret .= xml::element( 'th', null, wfMsg( 'userrights-' . $name . '-col' ) );
+ }
+ $ret.= "</tr>\n<tr>\n";
+ foreach( $columns as $column ) {
+ if( $column === array() )
+ continue;
+ $ret .= "\t<td style='vertical-align:top;'>\n";
+ foreach( $column as $group => $checkbox ) {
+ $attr = $checkbox['disabled'] ? array( 'disabled' => 'disabled' ) : array();
+
+ if ( $checkbox['irreversible'] ) {
+ $text = htmlspecialchars( wfMsg( 'userrights-irreversible-marker',
+ User::getGroupMember( $group ) ) );
+ } else {
+ $text = htmlspecialchars( User::getGroupMember( $group ) );
+ }
+ $checkboxHtml = Xml::checkLabel( $text, "wpGroup-" . $group,
+ "wpGroup-" . $group, $checkbox['set'], $attr );
+ $ret .= "\t\t" . ( $checkbox['disabled']
+ ? Xml::tags( 'span', array( 'class' => 'mw-userrights-disabled' ), $checkboxHtml )
+ : $checkboxHtml
+ ) . "<br />\n";
}
- $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
+ $ret .= "\t</td>\n";
}
+ $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' );
return $ret;
}
@@ -502,7 +549,7 @@ class UserrightsPage extends SpecialPage {
// $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'] ));
+ return in_array( $group, $groups['remove'] ) || ( $this->isself && in_array( $group, $groups['remove-self'] ) );
}
/**
@@ -511,116 +558,17 @@ class UserrightsPage extends SpecialPage {
*/
private function canAdd( $group ) {
$groups = $this->changeableGroups();
- return in_array( $group, $groups['add'] ) || ($this->isself && in_array( $group, $groups['add-self'] ));
+ 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.
+ * Returns $wgUser->changeableGroups()
*
* @return Array array( 'add' => array( addablegroups ), 'remove' => array( removablegroups ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) )
*/
function changeableGroups() {
global $wgUser;
-
- 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' => array(),
- 'remove-self' => array() );
- $addergroups = $wgUser->getEffectiveGroups();
-
- foreach ($addergroups as $addergroup) {
- $groups = array_merge_recursive(
- $groups, $this->changeableByGroup($addergroup)
- );
- $groups['add'] = array_unique( $groups['add'] );
- $groups['remove'] = array_unique( $groups['remove'] );
- $groups['add-self'] = array_unique( $groups['add-self'] );
- $groups['remove-self'] = array_unique( $groups['remove-self'] );
- }
-
- // Run a hook because we can
- wfRunHooks( 'UserrightsChangeableGroups', array( $this, $wgUser, $addergroups, &$groups ) );
-
- return $groups;
- }
-
- /**
- * 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 ) , 'add-self' => array( addablegroups to self), 'remove-self' => array( removable groups from self) )
- */
- private function changeableByGroup( $group ) {
- global $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf;
-
- $groups = array( 'add' => array(), 'remove' => array(), 'add-self' => array(), 'remove-self' => array() );
- if( empty($wgAddGroups[$group]) ) {
- // Don't add anything to $groups
- } elseif( $wgAddGroups[$group] === true ) {
- // 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];
- }
-
- // Re-map numeric keys of AddToSelf/RemoveFromSelf to the 'user' key for backwards compatibility
- if( empty($wgGroupsAddToSelf['user']) || $wgGroupsAddToSelf['user'] !== true ) {
- foreach($wgGroupsAddToSelf as $key => $value) {
- if( is_int($key) ) {
- $wgGroupsAddToSelf['user'][] = $value;
- }
- }
- }
-
- if( empty($wgGroupsRemoveFromSelf['user']) || $wgGroupsRemoveFromSelf['user'] !== true ) {
- foreach($wgGroupsRemoveFromSelf as $key => $value) {
- if( is_int($key) ) {
- $wgGroupsRemoveFromSelf['user'][] = $value;
- }
- }
- }
-
- // Now figure out what groups the user can add to him/herself
- if( empty($wgGroupsAddToSelf[$group]) ) {
- } elseif( $wgGroupsAddToSelf[$group] === true ) {
- // No idea WHY this would be used, but it's there
- $groups['add-self'] = User::getAllGroups();
- } elseif( is_array($wgGroupsAddToSelf[$group]) ) {
- $groups['add-self'] = $wgGroupsAddToSelf[$group];
- }
-
- if( empty($wgGroupsRemoveFromSelf[$group]) ) {
- } elseif( $wgGroupsRemoveFromSelf[$group] === true ) {
- $groups['remove-self'] = User::getAllGroups();
- } elseif( is_array($wgGroupsRemoveFromSelf[$group]) ) {
- $groups['remove-self'] = $wgGroupsRemoveFromSelf[$group];
- }
-
- return $groups;
+ return $wgUser->changeableGroups();
}
/**
diff --git a/includes/specials/SpecialVersion.php b/includes/specials/SpecialVersion.php
index 95e06f4b..7da6023e 100644
--- a/includes/specials/SpecialVersion.php
+++ b/includes/specials/SpecialVersion.php
@@ -12,21 +12,29 @@
class SpecialVersion extends SpecialPage {
private $firstExtOpened = true;
+ static $viewvcUrls = array(
+ 'svn+ssh://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
+ 'http://svn.wikimedia.org/svnroot/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
+ # Doesn't work at the time of writing but maybe some day:
+ 'https://svn.wikimedia.org/viewvc/mediawiki' => 'http://svn.wikimedia.org/viewvc/mediawiki',
+ );
+
function __construct(){
- parent::__construct( 'Version' );
+ parent::__construct( 'Version' );
}
/**
* main()
*/
function execute( $par ) {
- global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks;
+ global $wgOut, $wgMessageCache, $wgSpecialVersionShowHooks, $wgContLang;
$wgMessageCache->loadAllMessages();
$this->setHeaders();
$this->outputHeader();
- $wgOut->addHTML( '<div dir="ltr">' );
+ $wgOut->addHTML( Xml::openElement( 'div',
+ array( 'dir' => $wgContLang->getDir() ) ) );
$text =
$this->MediaWikiCredits() .
$this->softwareInformation() .
@@ -47,13 +55,19 @@ class SpecialVersion extends SpecialPage {
* @return wiki text showing the license information
*/
static function MediaWikiCredits() {
- $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) ) .
- "__NOTOC__
+ global $wgContLang;
+
+ $ret = Xml::element( 'h2', array( 'id' => 'mw-version-license' ), wfMsg( 'version-license' ) );
+
+ // This text is always left-to-right.
+ $ret .= '<div dir="ltr">';
+ $ret .= "__NOTOC__
This wiki is powered by '''[http://www.mediawiki.org/ MediaWiki]''',
- copyright (C) 2001-2009 Magnus Manske, Brion Vibber, Lee Daniel Crocker,
+ copyright © 2001-2010 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.
+ Aaron Schulz, Andrew Garrett, Raimond Spekking, Alexandre Emsenhuber,
+ Siebrand Mazeland, Chad Horohoe 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
@@ -70,6 +84,7 @@ class SpecialVersion extends SpecialPage {
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].
";
+ $ret .= '</div>';
return str_replace( "\t\t", '', $ret ) . "\n";
}
@@ -80,25 +95,30 @@ class SpecialVersion extends SpecialPage {
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' ) ) .
+ // Put the software in an array of form 'name' => 'version'. All messages should
+ // be loaded here, so feel free to use wfMsg*() in the 'name'. Raw HTML or wikimarkup
+ // can be used
+ $software = array();
+ $software['[http://www.mediawiki.org/ MediaWiki]'] = self::getVersionLinked();
+ $software['[http://www.php.net/ PHP]'] = phpversion() . " (" . php_sapi_name() . ")";
+ $software[$dbr->getSoftwareLink()] = $dbr->getServerVersion();
+
+ // Allow a hook to add/remove items
+ wfRunHooks( 'SoftwareInfo', array( &$software ) );
+
+ $out = Xml::element( 'h2', array( 'id' => 'mw-version-software' ), wfMsg( 'version-software' ) ) .
+ Xml::openElement( 'table', array( 'class' => 'wikitable', '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' );
+ </tr>\n";
+ foreach( $software as $name => $version ) {
+ $out .= "<tr>
+ <td>" . $name . "</td>
+ <td>" . $version . "</td>
+ </tr>\n";
+ }
+ return $out . Xml::closeElement( 'table' );
}
/**
@@ -106,27 +126,52 @@ class SpecialVersion extends SpecialPage {
*
* @return mixed
*/
- public static function getVersion() {
+ public static function getVersion( $flags = '' ) {
global $wgVersion, $IP;
wfProfileIn( __METHOD__ );
- $svn = self::getSvnRevision( $IP );
- $version = $svn ? "$wgVersion (r$svn)" : $wgVersion;
+
+ $info = self::getSvnInfo( $IP );
+ if ( !$info ) {
+ $version = $wgVersion;
+ } elseif( $flags === 'nodb' ) {
+ $version = "$wgVersion (r{$info['checkout-rev']})";
+ } else {
+ $version = $wgVersion . ' ' .
+ wfMsg(
+ 'version-svn-revision',
+ isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
+ $info['checkout-rev']
+ );
+ }
+
wfProfileOut( __METHOD__ );
return $version;
}
/**
- * Return a string of the MediaWiki version with a link to SVN revision if
- * available
+ * Return a wikitext-formatted string of the MediaWiki version with a link to
+ * the 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;
+ $info = self::getSvnInfo( $IP );
+ if ( isset( $info['checkout-rev'] ) ) {
+ $linkText = wfMsg(
+ 'version-svn-revision',
+ isset( $info['directory-rev'] ) ? $info['directory-rev'] : '',
+ $info['checkout-rev']
+ );
+ if ( isset( $info['viewvc-url'] ) ) {
+ $version = "$wgVersion [{$info['viewvc-url']} $linkText]";
+ } else {
+ $version = "$wgVersion $linkText";
+ }
+ } else {
+ $version = $wgVersion;
+ }
wfProfileOut( __METHOD__ );
return $version;
}
@@ -148,64 +193,40 @@ class SpecialVersion extends SpecialPage {
wfRunHooks( 'SpecialVersionExtensionTypes', array( &$this, &$extensionTypes ) );
$out = Xml::element( 'h2', array( 'id' => 'mw-version-ext' ), wfMsg( 'version-extensions' ) ) .
- Xml::openElement( 'table', array( 'id' => 'sv-ext' ) );
+ Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-ext' ) );
foreach ( $extensionTypes as $type => $text ) {
if ( isset ( $wgExtensionCredits[$type] ) && count ( $wgExtensionCredits[$type] ) ) {
- $out .= $this->openExtType( $text );
+ $out .= $this->openExtType( $text, 'credits-' . $type );
usort( $wgExtensionCredits[$type], array( $this, 'compare' ) );
foreach ( $wgExtensionCredits[$type] as $extension ) {
- $version = null;
- $subVersion = '';
- if ( isset( $extension['version'] ) ) {
- $version = $extension['version'];
- }
- if ( isset( $extension['svn-revision'] ) &&
- preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/',
- $extension['svn-revision'], $m ) ) {
- $subVersion = 'r' . $m[1];
- }
-
- if( $version && $subVersion ) {
- $version = $version . ' [' . $subVersion . ']';
- } elseif ( !$version && $subVersion ) {
- $version = $subVersion;
- }
-
- $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'] : ''
- );
+ $out .= $this->formatCredits( $extension );
}
}
}
if ( count( $wgExtensionFunctions ) ) {
- $out .= $this->openExtType( wfMsg( 'version-extension-functions' ) );
- $out .= '<tr><td colspan="3">' . $this->listToText( $wgExtensionFunctions ) . "</td></tr>\n";
+ $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' );
+ $out .= '<tr><td colspan="4">' . $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";
+ $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' );
+ $out .= '<tr><td colspan="4">' . $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";
+ $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' );
+ $out .= '<tr><td colspan="4">' . $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 .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ), 'skin-extension-functions' );
+ $out .= '<tr><td colspan="4">' . $this->listToText( $wgSkinExtensionFunctions ) . "</td></tr>\n";
}
$out .= Xml::closeElement( 'table' );
return $out;
@@ -223,23 +244,72 @@ class SpecialVersion extends SpecialPage {
}
}
- 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)" : '';
+ function formatCredits( $extension ) {
+ $name = isset( $extension['name'] ) ? $extension['name'] : '[no name]';
+ if ( isset( $extension['path'] ) ) {
+ $svnInfo = self::getSvnInfo( dirname($extension['path']) );
+ $directoryRev = isset( $svnInfo['directory-rev'] ) ? $svnInfo['directory-rev'] : null;
+ $checkoutRev = isset( $svnInfo['checkout-rev'] ) ? $svnInfo['checkout-rev'] : null;
+ $viewvcUrl = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : null;
+ } else {
+ $directoryRev = null;
+ $checkoutRev = null;
+ $viewvcUrl = null;
+ }
+
+ # Make main link (or just the name if there is no URL)
+ if ( isset( $extension['url'] ) ) {
+ $mainLink = "[{$extension['url']} $name]";
+ } else {
+ $mainLink = $name;
+ }
+ if ( isset( $extension['version'] ) ) {
+ $versionText = '<span class="mw-version-ext-version">' .
+ wfMsg( 'version-version', $extension['version'] ) .
+ '</span>';
+ } else {
+ $versionText = '';
+ }
- # Look for a localized description
- if( isset( $descriptionMsg ) ) {
- $msg = wfMsg( $descriptionMsg );
- if ( !wfEmptyMsg( $descriptionMsg, $msg ) && $msg != '' ) {
- $description = $msg;
+ # Make subversion text/link
+ if ( $checkoutRev ) {
+ $svnText = wfMsg( 'version-svn-revision', $directoryRev, $checkoutRev );
+ $svnText = isset( $viewvcUrl ) ? "[$viewvcUrl $svnText]" : $svnText;
+ } else {
+ $svnText = false;
+ }
+
+ # Make description text
+ $description = isset ( $extension['description'] ) ? $extension['description'] : '';
+ if( isset ( $extension['descriptionmsg'] ) ) {
+ # Look for a localized description
+ $descriptionMsg = $extension['descriptionmsg'];
+ if( is_array( $descriptionMsg ) ) {
+ $descriptionMsgKey = $descriptionMsg[0]; // Get the message key
+ array_shift( $descriptionMsg ); // Shift out the message key to get the parameters only
+ array_map( "htmlspecialchars", $descriptionMsg ); // For sanity
+ $msg = wfMsg( $descriptionMsgKey, $descriptionMsg );
+ } else {
+ $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>
+ if ( $svnText !== false ) {
+ $extNameVer = "<tr>
+ <td><em>$mainLink $versionText</em></td>
+ <td><em>$svnText</em></td>";
+ } else {
+ $extNameVer = "<tr>
+ <td colspan=\"2\"><em>$mainLink $versionText</em></td>";
+ }
+ $author = isset ( $extension['author'] ) ? $extension['author'] : array();
+ $extDescAuthor = "<td>$description</td>
+ <td>" . $this->listToText( (array)$author, false ) . "</td>
</tr>\n";
+ return $extNameVer . $extDescAuthor;
}
/**
@@ -253,7 +323,7 @@ class SpecialVersion extends SpecialPage {
ksort( $myWgHooks );
$ret = Xml::element( 'h2', array( 'id' => 'mw-version-hooks' ), wfMsg( 'version-hooks' ) ) .
- Xml::openElement( 'table', array( 'id' => 'sv-hooks' ) ) .
+ Xml::openElement( 'table', array( 'class' => 'wikitable', 'id' => 'sv-hooks' ) ) .
"<tr>
<th>" . wfMsg( 'version-hook-name' ) . "</th>
<th>" . wfMsg( 'version-hook-subscribedby' ) . "</th>
@@ -271,19 +341,20 @@ class SpecialVersion extends SpecialPage {
return '';
}
- private function openExtType($text, $name = null) {
- $opt = array( 'colspan' => 3 );
+ private function openExtType( $text, $name = null ) {
+ $opt = array( 'colspan' => 4 );
$out = '';
- if(!$this->firstExtOpened) {
+ 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"; }
+ if( $name )
+ $opt['id'] = "sv-$name";
- $out .= "<tr>" . Xml::element( 'th', $opt, $text) . "</tr>\n";
+ $out .= "<tr>" . Xml::element( 'th', $opt, $text ) . "</tr>\n";
return $out;
}
@@ -298,9 +369,10 @@ class SpecialVersion extends SpecialPage {
/**
* @param array $list
+ * @param bool $sort
* @return string
*/
- function listToText( $list ) {
+ function listToText( $list, $sort = true ) {
$cnt = count( $list );
if ( $cnt == 1 ) {
@@ -310,7 +382,9 @@ class SpecialVersion extends SpecialPage {
return '';
} else {
global $wgLang;
- sort( $list );
+ if ( $sort ) {
+ sort( $list );
+ }
return $wgLang->listToText( array_map( array( __CLASS__, 'arrayToString' ), $list ) );
}
}
@@ -338,12 +412,20 @@ class SpecialVersion extends SpecialPage {
}
/**
- * Retrieve the revision number of a Subversion working directory.
+ * Get an associative array of information about a given path, from its .svn
+ * subdirectory. Returns false on error, such as if the directory was not
+ * checked out with subversion.
*
- * @param string $dir
- * @return mixed revision number as int, or false if not a SVN checkout
+ * Returned keys are:
+ * Required:
+ * checkout-rev The revision which was checked out
+ * Optional:
+ * directory-rev The revision when the directory was last modified
+ * url The subversion URL of the directory
+ * repo-url The base URL of the repository
+ * viewvc-url A ViewVC URL pointing to the checked-out revision
*/
- public static function getSvnRevision( $dir ) {
+ public static function getSvnInfo( $dir ) {
// http://svnbook.red-bean.com/nightly/en/svn.developer.insidewc.html
$entries = $dir . '/.svn/entries';
@@ -351,10 +433,13 @@ class SpecialVersion extends SpecialPage {
return false;
}
- $content = file( $entries );
+ $lines = file( $entries );
+ if ( !count( $lines ) ) {
+ return false;
+ }
// check if file is xml (subversion release <= 1.3) or not (subversion release = 1.4)
- if( preg_match( '/^<\?xml/', $content[0] ) ) {
+ if( preg_match( '/^<\?xml/', $lines[0] ) ) {
// subversion is release <= 1.3
if( !function_exists( 'simplexml_load_file' ) ) {
// We could fall back to expat... YUCK
@@ -371,15 +456,52 @@ class SpecialVersion extends SpecialPage {
if( $xml->entry[0]['name'] == '' ) {
// The directory entry should always have a revision marker.
if( $entry['revision'] ) {
- return intval( $entry['revision'] );
+ return array( 'checkout-rev' => intval( $entry['revision'] ) );
}
}
}
}
return false;
+ }
+
+ // subversion is release 1.4 or above
+ if ( count( $lines ) < 11 ) {
+ return false;
+ }
+ $info = array(
+ 'checkout-rev' => intval( trim( $lines[3] ) ),
+ 'url' => trim( $lines[4] ),
+ 'repo-url' => trim( $lines[5] ),
+ 'directory-rev' => intval( trim( $lines[10] ) )
+ );
+ if ( isset( self::$viewvcUrls[$info['repo-url']] ) ) {
+ $viewvc = str_replace(
+ $info['repo-url'],
+ self::$viewvcUrls[$info['repo-url']],
+ $info['url']
+ );
+ $pathRelativeToRepo = substr( $info['url'], strlen( $info['repo-url'] ) );
+ $viewvc .= '/?pathrev=';
+ $viewvc .= urlencode( $info['checkout-rev'] );
+ $info['viewvc-url'] = $viewvc;
+ }
+ return $info;
+ }
+
+ /**
+ * Retrieve the revision number of a Subversion working directory.
+ *
+ * @param String $dir Directory of the svn checkout
+ * @return int revision number as int
+ */
+ public static function getSvnRevision( $dir ) {
+ $info = self::getSvnInfo( $dir );
+ if ( $info === false ) {
+ return false;
+ } elseif ( isset( $info['checkout-rev'] ) ) {
+ return $info['checkout-rev'];
} else {
- // subversion is release 1.4
- return intval( $content[3] );
+ return false;
}
}
diff --git a/includes/specials/SpecialWantedcategories.php b/includes/specials/SpecialWantedcategories.php
index 7497f9be..5e5a4f17 100644
--- a/includes/specials/SpecialWantedcategories.php
+++ b/includes/specials/SpecialWantedcategories.php
@@ -13,20 +13,12 @@
* @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 {
+class WantedCategoriesPage extends WantedQueryPage {
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' );
@@ -45,32 +37,21 @@ class WantedCategoriesPage extends QueryPage {
";
}
- 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() );
+ $text = htmlspecialchars( $wgContLang->convert( $nt->getText() ) );
$plink = $this->isCached() ?
- $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
- $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
+ $skin->link( $nt, $text ) :
+ $skin->link(
+ $nt,
+ $text,
+ array(),
+ array(),
+ array( 'broken' )
+ );
$nlinks = wfMsgExt( 'nmembers', array( 'parsemag', 'escape'),
$wgLang->formatNum( $result->value ) );
diff --git a/includes/specials/SpecialWantedfiles.php b/includes/specials/SpecialWantedfiles.php
index 4957531e..189b9d8b 100644
--- a/includes/specials/SpecialWantedfiles.php
+++ b/includes/specials/SpecialWantedfiles.php
@@ -13,20 +13,12 @@
* @copyright Copyright © 2008, Soxred93
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
-class WantedFilesPage extends QueryPage {
+class WantedFilesPage extends WantedQueryPage {
function getName() {
return 'Wantedfiles';
}
- function isExpensive() {
- return true;
- }
-
- function isSyndicated() {
- return false;
- }
-
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
list( $imagelinks, $page ) = $dbr->tableNamesN( 'imagelinks', 'page' );
@@ -44,55 +36,6 @@ class WantedFilesPage extends QueryPage {
GROUP BY il_to
";
}
-
- function sortDescending() { return true; }
-
- /**
- * Fetch user page links and cache their existence
- */
- function preprocessResults( $db, $res ) {
- $batch = new LinkBatch;
- while ( $row = $db->fetchObject( $res ) )
- $batch->add( $row->namespace, $row->title );
- $batch->execute();
-
- // Back to start for display
- if ( $db->numRows( $res ) > 0 )
- // If there are no rows we get an error seeking.
- $db->dataSeek( $res, 0 );
- }
-
- function formatResult( $skin, $result ) {
- global $wgLang, $wgContLang;
-
- $nt = Title::makeTitle( $result->namespace, $result->title );
- $text = $wgContLang->convert( $nt->getText() );
-
- $plink = $this->isCached() ?
- $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
- $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
-
- return wfSpecialList(
- $plink,
- $this->makeWlhLink( $nt, $skin, $result )
- );
- }
-
- /**
- * Make a "what links here" link for a given title
- *
- * @param Title $title Title to make the link for
- * @param Skin $skin Skin to use
- * @param object $result Result row
- * @return string
- */
- private function makeWlhLink( $title, $skin, $result ) {
- global $wgLang;
- $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
- $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
- $wgLang->formatNum( $result->value ) );
- return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) );
- }
}
/**
diff --git a/includes/specials/SpecialWantedpages.php b/includes/specials/SpecialWantedpages.php
index 7307b335..eeca87ab 100644
--- a/includes/specials/SpecialWantedpages.php
+++ b/includes/specials/SpecialWantedpages.php
@@ -8,7 +8,7 @@
* implements Special:Wantedpages
* @ingroup SpecialPage
*/
-class WantedPagesPage extends QueryPage {
+class WantedPagesPage extends WantedQueryPage {
var $nlinks;
function WantedPagesPage( $inc = false, $nlinks = true ) {
@@ -20,11 +20,6 @@ class WantedPagesPage extends QueryPage {
return 'Wantedpages';
}
- function isExpensive() {
- return true;
- }
- function isSyndicated() { return false; }
-
function getSQL() {
global $wgWantedPagesThreshold;
$count = $wgWantedPagesThreshold - 1;
@@ -32,83 +27,23 @@ class WantedPagesPage extends QueryPage {
$pagelinks = $dbr->tableName( 'pagelinks' );
$page = $dbr->tableName( 'page' );
$sql = "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";
+ 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 ( " . NS_USER . ", ". NS_USER_TALK . ")
+ AND pg2.page_namespace != " . NS_MEDIAWIKI . "
+ GROUP BY pl_namespace, pl_title
+ HAVING COUNT(*) > $count";
wfRunHooks( 'WantedPages::getSQL', array( &$this, &$sql ) );
return $sql;
}
-
- /**
- * 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 wfMsg( 'wantedpages-badtitle', $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;
- }
- }
-
}
/**
diff --git a/includes/specials/SpecialWantedtemplates.php b/includes/specials/SpecialWantedtemplates.php
index 7dd9a262..329d7a3f 100644
--- a/includes/specials/SpecialWantedtemplates.php
+++ b/includes/specials/SpecialWantedtemplates.php
@@ -15,20 +15,12 @@
* @copyright Copyright © 2008, Danny B.
* @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
*/
-class WantedTemplatesPage extends QueryPage {
+class WantedTemplatesPage extends WantedQueryPage {
function getName() {
return 'Wantedtemplates';
}
- function isExpensive() {
- return true;
- }
-
- function isSyndicated() {
- return false;
- }
-
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
list( $templatelinks, $page ) = $dbr->tableNamesN( 'templatelinks', 'page' );
@@ -45,55 +37,6 @@ class WantedTemplatesPage extends QueryPage {
GROUP BY tl_namespace, tl_title
";
}
-
- function sortDescending() { return true; }
-
- /**
- * Fetch user page links and cache their existence
- */
- function preprocessResults( $db, $res ) {
- $batch = new LinkBatch;
- while ( $row = $db->fetchObject( $res ) )
- $batch->add( $row->namespace, $row->title );
- $batch->execute();
-
- // Back to start for display
- if ( $db->numRows( $res ) > 0 )
- // If there are no rows we get an error seeking.
- $db->dataSeek( $res, 0 );
- }
-
- function formatResult( $skin, $result ) {
- global $wgLang, $wgContLang;
-
- $nt = Title::makeTitle( $result->namespace, $result->title );
- $text = $wgContLang->convert( $nt->getText() );
-
- $plink = $this->isCached() ?
- $skin->makeLinkObj( $nt, htmlspecialchars( $text ) ) :
- $skin->makeBrokenLinkObj( $nt, htmlspecialchars( $text ) );
-
- return wfSpecialList(
- $plink,
- $this->makeWlhLink( $nt, $skin, $result )
- );
- }
-
- /**
- * Make a "what links here" link for a given title
- *
- * @param Title $title Title to make the link for
- * @param Skin $skin Skin to use
- * @param object $result Result row
- * @return string
- */
- private function makeWlhLink( $title, $skin, $result ) {
- global $wgLang;
- $wlh = SpecialPage::getTitleFor( 'Whatlinkshere' );
- $label = wfMsgExt( 'nlinks', array( 'parsemag', 'escape' ),
- $wgLang->formatNum( $result->value ) );
- return $skin->link( $wlh, $label, array(), array( 'target' => $title->getPrefixedText() ) );
- }
}
/**
diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php
index b14577b5..c32af2ae 100644
--- a/includes/specials/SpecialWatchlist.php
+++ b/includes/specials/SpecialWatchlist.php
@@ -12,7 +12,25 @@
function wfSpecialWatchlist( $par ) {
global $wgUser, $wgOut, $wgLang, $wgRequest;
global $wgRCShowWatchingUsers, $wgEnotifWatchlist, $wgShowUpdatedMarker;
- global $wgEnotifWatchlist;
+
+ // Add feed links
+ $wlToken = $wgUser->getOption( 'watchlisttoken' );
+ if (!$wlToken) {
+ $wlToken = sha1( mt_rand() . microtime( true ) );
+ $wgUser->setOption( 'watchlisttoken', $wlToken );
+ $wgUser->saveSettings();
+ }
+
+ global $wgServer, $wgScriptPath, $wgFeedClasses;
+ $apiParams = array( 'action' => 'feedwatchlist', 'allrev' => 'allrev',
+ 'wlowner' => $wgUser->getName(), 'wltoken' => $wlToken );
+ $feedTemplate = wfScript('api').'?';
+
+ foreach( $wgFeedClasses as $format => $class ) {
+ $theseParams = $apiParams + array( 'feedformat' => $format );
+ $url = $feedTemplate . wfArrayToCGI( $theseParams );
+ $wgOut->addFeedLink( $format, $url );
+ }
$skin = $wgUser->getSkin();
$specialTitle = SpecialPage::getTitleFor( 'Watchlist' );
@@ -21,8 +39,12 @@ function wfSpecialWatchlist( $par ) {
# Anons don't get a watchlist
if( $wgUser->isAnon() ) {
$wgOut->setPageTitle( wfMsg( 'watchnologin' ) );
- $llink = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Userlogin' ),
- wfMsgHtml( 'loginreqlink' ), 'returnto=' . $specialTitle->getPrefixedUrl() );
+ $llink = $skin->linkKnown(
+ SpecialPage::getTitleFor( 'Userlogin' ),
+ wfMsgHtml( 'loginreqlink' ),
+ array(),
+ array( 'returnto' => $specialTitle->getPrefixedText() )
+ );
$wgOut->addHTML( wfMsgWikiHtml( 'watchlistanontext', $llink ) );
return;
}
@@ -248,42 +270,17 @@ function wfSpecialWatchlist( $par ) {
$cutofflinks = "\n" . wlCutoffLinks( $days, 'Watchlist', $nondefaults ) . "<br />\n";
- # Spit out some control panel links
$thisTitle = SpecialPage::getTitleFor( 'Watchlist' );
- $skin = $wgUser->getSkin();
- $showLinktext = wfMsgHtml( 'show' );
- $hideLinktext = wfMsgHtml( 'hide' );
- # Hide/show minor edits
- $label = $hideMinor ? $showLinktext : $hideLinktext;
- $linkBits = wfArrayToCGI( array( 'hideMinor' => 1 - (int)$hideMinor ), $nondefaults );
- $links[] = wfMsgHtml( 'rcshowhideminor', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
-
- # Hide/show bot edits
- $label = $hideBots ? $showLinktext : $hideLinktext;
- $linkBits = wfArrayToCGI( array( 'hideBots' => 1 - (int)$hideBots ), $nondefaults );
- $links[] = wfMsgHtml( 'rcshowhidebots', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
-
- # Hide/show anonymous edits
- $label = $hideAnons ? $showLinktext : $hideLinktext;
- $linkBits = wfArrayToCGI( array( 'hideAnons' => 1 - (int)$hideAnons ), $nondefaults );
- $links[] = wfMsgHtml( 'rcshowhideanons', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
-
- # Hide/show logged in edits
- $label = $hideLiu ? $showLinktext : $hideLinktext;
- $linkBits = wfArrayToCGI( array( 'hideLiu' => 1 - (int)$hideLiu ), $nondefaults );
- $links[] = wfMsgHtml( 'rcshowhideliu', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
-
- # Hide/show own edits
- $label = $hideOwn ? $showLinktext : $hideLinktext;
- $linkBits = wfArrayToCGI( array( 'hideOwn' => 1 - (int)$hideOwn ), $nondefaults );
- $links[] = wfMsgHtml( 'rcshowhidemine', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
-
- # Hide/show patrolled edits
+ # Spit out some control panel links
+ $links[] = wlShowHideLink( $nondefaults, 'rcshowhideminor', 'hideMinor', $hideMinor );
+ $links[] = wlShowHideLink( $nondefaults, 'rcshowhidebots', 'hideBots', $hideBots );
+ $links[] = wlShowHideLink( $nondefaults, 'rcshowhideanons', 'hideAnons', $hideAnons );
+ $links[] = wlShowHideLink( $nondefaults, 'rcshowhideliu', 'hideLiu', $hideLiu );
+ $links[] = wlShowHideLink( $nondefaults, 'rcshowhidemine', 'hideOwn', $hideOwn );
+
if( $wgUser->useRCPatrol() ) {
- $label = $hidePatrolled ? $showLinktext : $hideLinktext;
- $linkBits = wfArrayToCGI( array( 'hidePatrolled' => 1 - (int)$hidePatrolled ), $nondefaults );
- $links[] = wfMsgHtml( 'rcshowhidepatr', $skin->makeKnownLinkObj( $thisTitle, $label, $linkBits ) );
+ $links[] = wlShowHideLink( $nondefaults, 'rcshowhidepatr', 'hidePatrolled', $hidePatrolled );
}
# Namespace filter and put the whole form together.
@@ -311,6 +308,8 @@ function wfSpecialWatchlist( $par ) {
$form .= Xml::closeElement( 'fieldset' );
$wgOut->addHTML( $form );
+ $wgOut->addHTML( ChangesList::flagLegend() );
+
# If there's nothing to show, stop here
if( $numRows == 0 ) {
$wgOut->addWikiMsg( 'watchnochange' );
@@ -334,7 +333,8 @@ function wfSpecialWatchlist( $par ) {
$dbr->dataSeek( $res, 0 );
$list = ChangesList::newFromUser( $wgUser );
-
+ $list->setWatchlistDivs();
+
$s = $list->beginRecentChangesList();
$counter = 1;
while ( $obj = $dbr->fetchObject( $res ) ) {
@@ -368,23 +368,53 @@ function wfSpecialWatchlist( $par ) {
$wgOut->addHTML( $s );
}
+function wlShowHideLink( $options, $message, $name, $value ) {
+ global $wgUser;
+
+ $showLinktext = wfMsgHtml( 'show' );
+ $hideLinktext = wfMsgHtml( 'hide' );
+ $title = SpecialPage::getTitleFor( 'Watchlist' );
+ $skin = $wgUser->getSkin();
+
+ $label = $value ? $showLinktext : $hideLinktext;
+ $options[$name] = 1 - (int) $value;
+
+ return wfMsgHtml( $message, $skin->linkKnown( $title, $label, array(), $options ) );
+}
+
+
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 ) );
+ $title = Title::newFromText( $wgContLang->specialPage( $page ) );
+ $options['days'] = ($h / 24.0);
+
+ $s = $sk->linkKnown(
+ $title,
+ $wgLang->formatNum( $h ),
+ array(),
+ $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 ) );
+ $title = Title::newFromText( $wgContLang->specialPage( $page ) );
+ $options['days'] = $d;
+ $message = ($d ? $wgLang->formatNum( $d ) : wfMsgHtml( 'watchlistall2' ) );
+
+ $s = $sk->linkKnown(
+ $title,
+ $message,
+ array(),
+ $options
+ );
+
return $s;
}
diff --git a/includes/specials/SpecialWhatlinkshere.php b/includes/specials/SpecialWhatlinkshere.php
index 3f485bd8..b63c0eee 100644
--- a/includes/specials/SpecialWhatlinkshere.php
+++ b/includes/specials/SpecialWhatlinkshere.php
@@ -7,40 +7,29 @@
*/
/**
- * 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;
+class SpecialWhatLinksHere extends SpecialPage {
// Stored objects
protected $opts, $target, $selfTitle;
// Stored globals
- protected $skin, $request;
+ protected $skin;
protected $limits = array( 20, 50, 100, 250, 500 );
- function WhatLinksHerePage( $request, $par = null ) {
+ public function __construct() {
+ parent::__construct( 'Whatlinkshere' );
global $wgUser;
- $this->request = $request;
$this->skin = $wgUser->getSkin();
- $this->par = $par;
}
- function execute() {
- global $wgOut;
+ function execute( $par ) {
+ global $wgOut, $wgRequest;
+
+ $this->setHeaders();
$opts = new FormOptions();
@@ -54,12 +43,12 @@ class WhatLinksHerePage {
$opts->add( 'hidelinks', false );
$opts->add( 'hideimages', false );
- $opts->fetchValuesFromRequest( $this->request );
+ $opts->fetchValuesFromRequest( $wgRequest );
$opts->validateIntBounds( 'limit', 0, 5000 );
// Give precedence to subpage syntax
- if ( isset($this->par) ) {
- $opts->setValue( 'target', $this->par );
+ if ( isset($par) ) {
+ $opts->setValue( 'target', $par );
}
// Bind to member variable
@@ -271,8 +260,18 @@ class WhatLinksHerePage {
}
}
- $suppressRedirect = $row->page_is_redirect ? 'redirect=no' : '';
- $link = $this->skin->makeKnownLinkObj( $nt, '', $suppressRedirect );
+ if( $row->page_is_redirect ) {
+ $query = array( 'redirect' => 'no' );
+ } else {
+ $query = array();
+ }
+
+ $link = $this->skin->linkKnown(
+ $nt,
+ null,
+ array(),
+ $query
+ );
// Display properties (redirect or template)
$propsText = '';
@@ -306,12 +305,21 @@ class WhatLinksHerePage {
if ( $title === null )
$title = SpecialPage::getTitleFor( 'Whatlinkshere' );
- $targetText = $target->getPrefixedUrl();
- return $this->skin->makeKnownLinkObj( $title, $text, 'target=' . $targetText );
+ return $this->skin->linkKnown(
+ $title,
+ $text,
+ array(),
+ array( 'target' => $target->getPrefixedText() )
+ );
}
function makeSelfLink( $text, $query ) {
- return $this->skin->makeKnownLinkObj( $this->selfTitle, $text, $query );
+ return $this->skin->linkKnown(
+ $this->selfTitle,
+ $text,
+ array(),
+ $query
+ );
}
function getPrevNext( $prevId, $nextId ) {
@@ -326,18 +334,18 @@ class WhatLinksHerePage {
if ( 0 != $prevId ) {
$overrides = array( 'from' => $this->opts->getValue( 'back' ) );
- $prev = $this->makeSelfLink( $prev, wfArrayToCGI( $overrides, $changed ) );
+ $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) );
}
if ( 0 != $nextId ) {
$overrides = array( 'from' => $nextId, 'back' => $prevId );
- $next = $this->makeSelfLink( $next, wfArrayToCGI( $overrides, $changed ) );
+ $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) );
}
$limitLinks = array();
foreach ( $this->limits as $limit ) {
$prettyLimit = $wgLang->formatNum( $limit );
$overrides = array( 'limit' => $limit );
- $limitLinks[] = $this->makeSelfLink( $prettyLimit, wfArrayToCGI( $overrides, $changed ) );
+ $limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) );
}
$nums = $wgLang->pipeList( $limitLinks );
@@ -346,7 +354,7 @@ class WhatLinksHerePage {
}
function whatlinkshereForm() {
- global $wgScript, $wgTitle;
+ global $wgScript;
// We get nicer value from the title object
$this->opts->consumeValue( 'target' );
@@ -360,7 +368,7 @@ class WhatLinksHerePage {
$f = Xml::openElement( 'form', array( 'action' => $wgScript ) );
# Values that should not be forgotten
- $f .= Xml::hidden( 'title', $wgTitle->getPrefixedText() );
+ $f .= Xml::hidden( 'title', SpecialPage::getTitleFor( 'Whatlinkshere' )->getPrefixedText() );
foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
$f .= Xml::hidden( $name, $value );
}
@@ -388,6 +396,11 @@ class WhatLinksHerePage {
return $f;
}
+ /**
+ * Create filter panel
+ *
+ * @return string HTML fieldset and filter panel with the show/hide links
+ */
function getFilterPanel() {
global $wgLang;
$show = wfMsgHtml( 'show' );
@@ -400,11 +413,14 @@ class WhatLinksHerePage {
$types = array( 'hidetrans', 'hidelinks', 'hideredirs' );
if( $this->target->getNamespace() == NS_FILE )
$types[] = 'hideimages';
+
+ // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans', 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
+ // To be sure they will be find by grep
foreach( $types as $type ) {
$chosen = $this->opts->getValue( $type );
- $msg = wfMsgHtml( "whatlinkshere-{$type}", $chosen ? $show : $hide );
+ $msg = $chosen ? $show : $hide;
$overrides = array( $type => !$chosen );
- $links[] = $this->makeSelfLink( $msg, wfArrayToCGI( $overrides, $changed ) );
+ $links[] = wfMsgHtml( "whatlinkshere-{$type}", $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) );
}
return Xml::fieldset( wfMsg( 'whatlinkshere-filters' ), $wgLang->pipeList( $links ) );
}
diff --git a/includes/specials/SpecialWithoutinterwiki.php b/includes/specials/SpecialWithoutinterwiki.php
index 2092e43b..a5d60d2f 100644
--- a/includes/specials/SpecialWithoutinterwiki.php
+++ b/includes/specials/SpecialWithoutinterwiki.php
@@ -53,7 +53,7 @@ class WithoutInterwikiPage extends PageQueryPage {
function getSQL() {
$dbr = wfGetDB( DB_SLAVE );
list( $page, $langlinks ) = $dbr->tableNamesN( 'page', 'langlinks' );
- $prefix = $this->prefix ? "AND page_title LIKE '" . $dbr->escapeLike( $this->prefix ) . "%'" : '';
+ $prefix = $this->prefix ? 'AND page_title' . $dbr->buildLike( $this->prefix , $dbr->anyString() ) : '';
return
"SELECT 'Withoutinterwiki' AS type,
page_namespace AS namespace,
@@ -75,13 +75,10 @@ class WithoutInterwikiPage extends PageQueryPage {
}
function wfSpecialWithoutinterwiki() {
- global $wgRequest, $wgContLang, $wgCapitalLinks;
+ global $wgRequest, $wgContLang;
list( $limit, $offset ) = wfCheckLimits();
- if( $wgCapitalLinks ) {
- $prefix = $wgContLang->ucfirst( $wgRequest->getVal( 'prefix' ) );
- } else {
- $prefix = $wgRequest->getVal( 'prefix' );
- }
+ // Only searching the mainspace anyway
+ $prefix = Title::capitalize( $wgRequest->getVal( 'prefix' ), NS_MAIN );
$wip = new WithoutInterwikiPage();
$wip->setPrefix( $prefix );
$wip->doQuery( $offset, $limit );
diff --git a/includes/templates/NoLocalSettings.php b/includes/templates/NoLocalSettings.php
index 42682d60..45b758a9 100644
--- a/includes/templates/NoLocalSettings.php
+++ b/includes/templates/NoLocalSettings.php
@@ -35,11 +35,11 @@ foreach( $topdirs as $dir ){
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>
+<html xmlns='http://www.w3.org/1999/xhtml' lang='en'>
<head>
<title>MediaWiki <?php echo htmlspecialchars( $wgVersion ) ?></title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
- <style type='text/css' media='screen, projection'>
+ <style type='text/css' media='screen'>
html, body {
color: #000;
background-color: #fff;
diff --git a/includes/templates/PHP4.php b/includes/templates/PHP4.php
index 058351a0..b071ebd5 100644
--- a/includes/templates/PHP4.php
+++ b/includes/templates/PHP4.php
@@ -27,11 +27,11 @@ if ( preg_match( '!^(.*)/config/[^/]*.php$!', $scriptUrl, $m ) ) {
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns='http://www.w3.org/1999/xhtml' xml:lang='en' lang='en'>
+<html xmlns='http://www.w3.org/1999/xhtml' lang='en'>
<head>
<title>MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?></title>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
- <style type='text/css' media='screen, projection'>
+ <style type='text/css' media='screen'>
html, body {
color: #000;
background-color: #fff;
diff --git a/includes/templates/Userlogin.php b/includes/templates/Userlogin.php
index 2ca9c3c4..60f33767 100644
--- a/includes/templates/Userlogin.php
+++ b/includes/templates/Userlogin.php
@@ -16,7 +16,7 @@ class UserloginTemplate extends QuickTemplate {
?>
<div class="<?php $this->text('messagetype') ?>box">
<?php if ( $this->data['messagetype'] == 'error' ) { ?>
- <h2><?php $this->msg('loginerror') ?></h2>
+ <strong><?php $this->msg( 'loginerror' )?></strong><br />
<?php } ?>
<?php $this->html('message') ?>
</div>
@@ -35,17 +35,30 @@ class UserloginTemplate extends QuickTemplate {
<tr>
<td class="mw-label"><label for='wpName1'><?php $this->msg('yourname') ?></label></td>
<td class="mw-input">
- <input type='text' class='loginText' name="wpName" id="wpName1"
- tabindex="1"
- value="<?php $this->text('name') ?>" size='20' />
+ <?php
+ echo Html::input( 'wpName', $this->data['name'], 'text', array(
+ 'class' => 'loginText',
+ 'id' => 'wpName1',
+ 'tabindex' => '1',
+ 'size' => '20',
+ 'required'
+ # Can't do + array( 'autofocus' ) because + for arrays in PHP
+ # only works right for associative arrays! Thanks, PHP.
+ ) + ( $this->data['name'] ? array() : array( 'autofocus' => '' ) ) ); ?>
+
</td>
</tr>
<tr>
<td class="mw-label"><label for='wpPassword1'><?php $this->msg('yourpassword') ?></label></td>
<td class="mw-input">
- <input type='password' class='loginPassword' name="wpPassword" id="wpPassword1"
- tabindex="2"
- value="" size='20' />
+ <?php
+ echo Html::input( 'wpPassword', null, 'password', array(
+ 'class' => 'loginPassword',
+ 'id' => 'wpPassword1',
+ 'tabindex' => '2',
+ 'size' => '20'
+ ) + ( $this->data['name'] ? array( 'autofocus' ) : array() ) ); ?>
+
</td>
</tr>
<?php if( $this->data['usedomain'] ) {
@@ -68,21 +81,32 @@ class UserloginTemplate extends QuickTemplate {
<tr>
<td></td>
<td class="mw-input">
- <input type='checkbox' name="wpRemember"
- tabindex="4"
- value="1" id="wpRemember"
- <?php if( $this->data['remember'] ) { ?>checked="checked"<?php } ?>
- /> <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
+ <?php
+ echo Html::input( 'wpRemember', '1', 'checkbox', array(
+ 'tabindex' => '4',
+ 'id' => 'wpRemember'
+ ) + ( $this->data['remember'] ? array( 'checked' ) : array() ) ); ?>
+
+ <label for="wpRemember"><?php $this->msg('remembermypassword') ?></label>
</td>
</tr>
- <?php } ?>
+<?php } ?>
<tr>
<td></td>
<td class="mw-submit">
- <input type='submit' name="wpLoginattempt" id="wpLoginattempt" tabindex="5" value="<?php $this->msg('login') ?>" />&nbsp;<?php if( $this->data['useemail'] && $this->data['canreset']) { ?><input type='submit' name="wpMailmypassword" id="wpMailmypassword"
- tabindex="6"
- value="<?php $this->msg('mailmypassword') ?>" />
- <?php } ?>
+ <?php
+ echo Html::input( 'wpLoginAttempt', wfMsg( 'login' ), 'submit', array(
+ 'id' => 'wpLoginAttempt',
+ 'tabindex' => '5'
+ ) );
+ if ( $this->data['useemail'] && $this->data['canreset'] ) {
+ echo '&nbsp;';
+ echo Html::input( 'wpMailmypassword', wfMsg( 'mailmypassword' ), 'submit', array(
+ 'id' => 'wpMailmypassword',
+ 'tabindex' => '6'
+ ) );
+ } ?>
+
</td>
</tr>
</table>
@@ -100,12 +124,13 @@ class UserloginTemplate extends QuickTemplate {
* @ingroup Templates
*/
class UsercreateTemplate extends QuickTemplate {
- function addInputItem( $name, $value, $type, $msg ) {
+ function addInputItem( $name, $value, $type, $msg, $helptext = false ) {
$this->data['extraInput'][] = array(
'name' => $name,
'value' => $value,
'type' => $type,
'msg' => $msg,
+ 'helptext' => $helptext,
);
}
@@ -114,7 +139,7 @@ class UsercreateTemplate extends QuickTemplate {
?>
<div class="<?php $this->text('messagetype') ?>box">
<?php if ( $this->data['messagetype'] == 'error' ) { ?>
- <h2><?php $this->msg('loginerror') ?></h2>
+ <strong><?php $this->msg( 'loginerror' )?></strong><br />
<?php } ?>
<?php $this->html('message') ?>
</div>
@@ -131,17 +156,27 @@ class UsercreateTemplate extends QuickTemplate {
<tr>
<td class="mw-label"><label for='wpName2'><?php $this->msg('yourname') ?></label></td>
<td class="mw-input">
- <input type='text' class='loginText' name="wpName" id="wpName2"
- tabindex="1"
- value="<?php $this->text('name') ?>" size='20' />
+ <?php
+ echo Html::input( 'wpName', $this->data['name'], 'text', array(
+ 'class' => 'loginText',
+ 'id' => 'wpName2',
+ 'tabindex' => '1',
+ 'size' => '20',
+ 'required',
+ 'autofocus'
+ ) ); ?>
</td>
</tr>
<tr>
<td class="mw-label"><label for='wpPassword2'><?php $this->msg('yourpassword') ?></label></td>
<td class="mw-input">
- <input type='password' class='loginPassword' name="wpPassword" id="wpPassword2"
- tabindex="2"
- value="" size='20' />
+<?php
+ echo Html::input( 'wpPassword', null, 'password', array(
+ 'class' => 'loginPassword',
+ 'id' => 'wpPassword2',
+ 'tabindex' => '2',
+ 'size' => '20'
+ ) + User::passwordChangeInputAttribs() ); ?>
</td>
</tr>
<?php if( $this->data['usedomain'] ) {
@@ -163,19 +198,26 @@ class UsercreateTemplate extends QuickTemplate {
<tr>
<td class="mw-label"><label for='wpRetype'><?php $this->msg('yourpasswordagain') ?></label></td>
<td class="mw-input">
- <input type='password' class='loginPassword' name="wpRetype" id="wpRetype"
- tabindex="4"
- value=""
- size='20' />
+ <?php
+ echo Html::input( 'wpRetype', null, 'password', array(
+ 'class' => 'loginPassword',
+ 'id' => 'wpRetype',
+ 'tabindex' => '4',
+ 'size' => '20'
+ ) + User::passwordChangeInputAttribs() ); ?>
</td>
</tr>
<tr>
<?php if( $this->data['useemail'] ) { ?>
<td class="mw-label"><label for='wpEmail'><?php $this->msg('youremail') ?></label></td>
<td class="mw-input">
- <input type='text' class='loginText' name="wpEmail" id="wpEmail"
- tabindex="5"
- value="<?php $this->text('email') ?>" size='20' />
+ <?php
+ echo Html::input( 'wpEmail', $this->data['email'], 'email', array(
+ 'class' => 'loginText',
+ 'id' => 'wpEmail',
+ 'tabindex' => '5',
+ 'size' => '20'
+ ) ); ?>
<div class="prefsectiontip">
<?php if( $this->data['emailrequired'] ) {
$this->msgWiki('prefs-help-email-required');
@@ -245,7 +287,12 @@ class UsercreateTemplate extends QuickTemplate {
<label for="<?php echo htmlspecialchars( $inputItem['name'] ); ?>"><?php
$this->msgHtml( $inputItem['msg'] ) ?></label><?php
}
+ if( $inputItem['helptext'] !== false ) {
?>
+ <div class="prefsectiontip">
+ <?php $this->msgWiki( $inputItem['helptext'] ); ?>
+ </div>
+ <?php } ?>
</td>
</tr>
<?php
diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php
new file mode 100644
index 00000000..5d955b36
--- /dev/null
+++ b/includes/upload/UploadBase.php
@@ -0,0 +1,1091 @@
+<?php
+/**
+ * @file
+ * @ingroup upload
+ *
+ * UploadBase and subclasses are the backend of MediaWiki's file uploads.
+ * The frontends are formed by ApiUpload and SpecialUpload.
+ *
+ * See also includes/docs/upload.txt
+ *
+ * @author Brion Vibber
+ * @author Bryan Tong Minh
+ * @author Michael Dale
+ */
+
+abstract class UploadBase {
+ protected $mTempPath;
+ protected $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType;
+ protected $mTitle = false, $mTitleError = 0;
+ protected $mFilteredName, $mFinalExtension;
+ protected $mLocalFile;
+
+ const SUCCESS = 0;
+ const OK = 0;
+ const EMPTY_FILE = 3;
+ const MIN_LENGTH_PARTNAME = 4;
+ const ILLEGAL_FILENAME = 5;
+ const OVERWRITE_EXISTING_FILE = 7;
+ const FILETYPE_MISSING = 8;
+ const FILETYPE_BADTYPE = 9;
+ const VERIFICATION_ERROR = 10;
+ const UPLOAD_VERIFICATION_ERROR = 11;
+ const HOOK_ABORTED = 11;
+
+ const SESSION_VERSION = 2;
+
+ /**
+ * Returns true if uploads are enabled.
+ * Can be override by subclasses.
+ */
+ public static function isEnabled() {
+ global $wgEnableUploads;
+ if ( !$wgEnableUploads ) {
+ return false;
+ }
+
+ # Check php's file_uploads setting
+ if( !wfIniGetBool( 'file_uploads' ) ) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if the user can use this upload module or else a string
+ * identifying the missing permission.
+ * Can be overriden by subclasses.
+ */
+ public static function isAllowed( $user ) {
+ if( !$user->isAllowed( 'upload' ) ) {
+ return 'upload';
+ }
+ return true;
+ }
+
+ // Upload handlers. Should probably just be a global.
+ static $uploadHandlers = array( 'Stash', 'File', 'Url' );
+
+ /**
+ * Create a form of UploadBase depending on wpSourceType and initializes it
+ */
+ public static function createFromRequest( &$request, $type = null ) {
+ $type = $type ? $type : $request->getVal( 'wpSourceType', 'File' );
+
+ if( !$type ) {
+ return null;
+ }
+
+ // Get the upload class
+ $type = ucfirst( $type );
+
+ // Give hooks the chance to handle this request
+ $className = null;
+ wfRunHooks( 'UploadCreateFromRequest', array( $type, &$className ) );
+ if ( is_null( $className ) ) {
+ $className = 'UploadFrom' . $type;
+ wfDebug( __METHOD__ . ": class name: $className\n" );
+ if( !in_array( $type, self::$uploadHandlers ) ) {
+ return null;
+ }
+ }
+
+ // Check whether this upload class is enabled
+ if( !call_user_func( array( $className, 'isEnabled' ) ) ) {
+ return null;
+ }
+
+ // Check whether the request is valid
+ if( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) {
+ return null;
+ }
+
+ $handler = new $className;
+
+ $handler->initializeFromRequest( $request );
+ return $handler;
+ }
+
+ /**
+ * Check whether a request if valid for this handler
+ */
+ public static function isValidRequest( $request ) {
+ return false;
+ }
+
+ public function __construct() {}
+
+ /**
+ * Initialize the path information
+ * @param $name string the desired destination name
+ * @param $tempPath string the temporary path
+ * @param $fileSize int the file size
+ * @param $removeTempFile bool (false) remove the temporary file?
+ * @return null
+ */
+ public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
+ $this->mDesiredDestName = $name;
+ $this->mTempPath = $tempPath;
+ $this->mFileSize = $fileSize;
+ $this->mRemoveTempFile = $removeTempFile;
+ }
+
+ /**
+ * Initialize from a WebRequest. Override this in a subclass.
+ */
+ public abstract function initializeFromRequest( &$request );
+
+ /**
+ * Fetch the file. Usually a no-op
+ */
+ public function fetchFile() {
+ return Status::newGood();
+ }
+
+ /**
+ * Return the file size
+ */
+ public function isEmptyFile() {
+ return empty( $this->mFileSize );
+ }
+
+ /**
+ * @param string $srcPath the source path
+ * @returns the real path if it was a virtual URL
+ */
+ function getRealPath( $srcPath ) {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ if ( $repo->isVirtualUrl( $srcPath ) ) {
+ return $repo->resolveVirtualUrl( $srcPath );
+ }
+ return $srcPath;
+ }
+
+ /**
+ * Verify whether the upload is sane.
+ * Returns self::OK or else an array with error information
+ */
+ public function verifyUpload() {
+ /**
+ * If there was no filename or a zero size given, give up quick.
+ */
+ if( $this->isEmptyFile() ) {
+ return array( 'status' => self::EMPTY_FILE );
+ }
+
+ /**
+ * Look at the contents of the file; if we can recognize the
+ * type but it's corrupt or data of the wrong type, we should
+ * probably not accept it.
+ */
+ $verification = $this->verifyFile();
+ if( $verification !== true ) {
+ if( !is_array( $verification ) ) {
+ $verification = array( $verification );
+ }
+ return array(
+ 'status' => self::VERIFICATION_ERROR,
+ 'details' => $verification
+ );
+ }
+
+ $nt = $this->getTitle();
+ if( is_null( $nt ) ) {
+ $result = array( 'status' => $this->mTitleError );
+ if( $this->mTitleError == self::ILLEGAL_FILENAME ) {
+ $result['filtered'] = $this->mFilteredName;
+ }
+ if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
+ $result['finalExt'] = $this->mFinalExtension;
+ }
+ return $result;
+ }
+ $this->mDestName = $this->getLocalFile()->getName();
+
+ /**
+ * In some cases we may forbid overwriting of existing files.
+ */
+ $overwrite = $this->checkOverwrite();
+ if( $overwrite !== true ) {
+ return array(
+ 'status' => self::OVERWRITE_EXISTING_FILE,
+ 'overwrite' => $overwrite
+ );
+ }
+
+ $error = '';
+ if( !wfRunHooks( 'UploadVerification',
+ array( $this->mDestName, $this->mTempPath, &$error ) ) ) {
+ // This status needs another name...
+ return array( 'status' => self::HOOK_ABORTED, 'error' => $error );
+ }
+
+ return array( 'status' => self::OK );
+ }
+
+ /**
+ * Verifies that it's ok to include the uploaded file
+ *
+ * @return mixed true of the file is verified, a string or array otherwise.
+ */
+ protected function verifyFile() {
+ $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
+ $this->checkMacBinary();
+
+ # magically determine mime type
+ $magic = MimeMagic::singleton();
+ $mime = $magic->guessMimeType( $this->mTempPath, false );
+
+ # check mime type, if desired
+ global $wgVerifyMimeType;
+ if ( $wgVerifyMimeType ) {
+ wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n");
+ if ( !$this->verifyExtension( $mime, $this->mFinalExtension ) ) {
+ return array( 'filetype-mime-mismatch' );
+ }
+
+ global $wgMimeTypeBlacklist;
+ if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
+ return array( 'filetype-badmime', $mime );
+ }
+
+ # Check IE type
+ $fp = fopen( $this->mTempPath, 'rb' );
+ $chunk = fread( $fp, 256 );
+ fclose( $fp );
+ $extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
+ $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
+ foreach ( $ieTypes as $ieType ) {
+ if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
+ return array( 'filetype-bad-ie-mime', $ieType );
+ }
+ }
+ }
+
+ # check for htmlish code and javascript
+ if( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
+ return 'uploadscripted';
+ }
+ if( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
+ if( self::detectScriptInSvg( $this->mTempPath ) ) {
+ return 'uploadscripted';
+ }
+ }
+
+ /**
+ * Scan the uploaded file for viruses
+ */
+ $virus = $this->detectVirus( $this->mTempPath );
+ if ( $virus ) {
+ return array( 'uploadvirus', $virus );
+ }
+ wfDebug( __METHOD__ . ": all clear; passing.\n" );
+ return true;
+ }
+
+ /**
+ * Check whether the user can edit, upload and create the image.
+ *
+ * @param User $user the user to verify the permissions against
+ * @return mixed An array as returned by getUserPermissionsErrors or true
+ * in case the user has proper permissions.
+ */
+ public function verifyPermissions( $user ) {
+ /**
+ * If the image is protected, non-sysop users won't be able
+ * to modify it by uploading a new revision.
+ */
+ $nt = $this->getTitle();
+ if( is_null( $nt ) ) {
+ return true;
+ }
+ $permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
+ $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user );
+ $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) );
+ if( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
+ $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
+ $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
+ return $permErrors;
+ }
+ return true;
+ }
+
+ /**
+ * Check for non fatal problems with the file
+ *
+ * @return array Array of warnings
+ */
+ public function checkWarnings() {
+ $warnings = array();
+
+ $localFile = $this->getLocalFile();
+ $filename = $localFile->getName();
+ $n = strrpos( $filename, '.' );
+ $partname = $n ? substr( $filename, 0, $n ) : $filename;
+
+ /**
+ * Check whether the resulting filename is different from the desired one,
+ * but ignore things like ucfirst() and spaces/underscore things
+ */
+ $comparableName = str_replace( ' ', '_', $this->mDesiredDestName );
+ $comparableName = Title::capitalize( $comparableName, NS_FILE );
+
+ if( $this->mDesiredDestName != $filename && $comparableName != $filename ) {
+ $warnings['badfilename'] = $filename;
+ }
+
+ // Check whether the file extension is on the unwanted list
+ global $wgCheckFileExtensions, $wgFileExtensions;
+ if ( $wgCheckFileExtensions ) {
+ if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) {
+ $warnings['filetype-unwanted-type'] = $this->mFinalExtension;
+ }
+ }
+
+ global $wgUploadSizeWarning;
+ if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) {
+ $warnings['large-file'] = $wgUploadSizeWarning;
+ }
+
+ if ( $this->mFileSize == 0 ) {
+ $warnings['emptyfile'] = true;
+ }
+
+ $exists = self::getExistsWarning( $localFile );
+ if( $exists !== false ) {
+ $warnings['exists'] = $exists;
+ }
+
+ // Check dupes against existing files
+ $hash = File::sha1Base36( $this->mTempPath );
+ $dupes = RepoGroup::singleton()->findBySha1( $hash );
+ $title = $this->getTitle();
+ // Remove all matches against self
+ foreach ( $dupes as $key => $dupe ) {
+ if( $title->equals( $dupe->getTitle() ) ) {
+ unset( $dupes[$key] );
+ }
+ }
+ if( $dupes ) {
+ $warnings['duplicate'] = $dupes;
+ }
+
+ // Check dupes against archives
+ $archivedImage = new ArchivedFile( null, 0, "{$hash}.{$this->mFinalExtension}" );
+ if ( $archivedImage->getID() > 0 ) {
+ $warnings['duplicate-archive'] = $archivedImage->getName();
+ }
+
+ return $warnings;
+ }
+
+ /**
+ * Really perform the upload. Stores the file in the local repo, watches
+ * if necessary and runs the UploadComplete hook.
+ *
+ * @return mixed Status indicating the whether the upload succeeded.
+ */
+ public function performUpload( $comment, $pageText, $watch, $user ) {
+ wfDebug( "\n\n\performUpload: sum:" . $comment . ' c: ' . $pageText . ' w:' . $watch );
+ $status = $this->getLocalFile()->upload( $this->mTempPath, $comment, $pageText,
+ File::DELETE_SOURCE, $this->mFileProps, false, $user );
+
+ if( $status->isGood() && $watch ) {
+ $user->addWatch( $this->getLocalFile()->getTitle() );
+ }
+
+ if( $status->isGood() ) {
+ wfRunHooks( 'UploadComplete', array( &$this ) );
+ }
+
+ return $status;
+ }
+
+ /**
+ * Returns the title of the file to be uploaded. Sets mTitleError in case
+ * the name was illegal.
+ *
+ * @return Title The title of the file or null in case the name was illegal
+ */
+ public function getTitle() {
+ if ( $this->mTitle !== false ) {
+ return $this->mTitle;
+ }
+
+ /**
+ * Chop off any directories in the given filename. Then
+ * filter out illegal characters, and try to make a legible name
+ * out of it. We'll strip some silently that Title would die on.
+ */
+ $basename = $this->mDesiredDestName;
+
+ $this->mFilteredName = wfStripIllegalFilenameChars( $basename );
+ /* Normalize to title form before we do any further processing */
+ $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
+ if( is_null( $nt ) ) {
+ $this->mTitleError = self::ILLEGAL_FILENAME;
+ return $this->mTitle = null;
+ }
+ $this->mFilteredName = $nt->getDBkey();
+
+ /**
+ * We'll want to blacklist against *any* 'extension', and use
+ * only the final one for the whitelist.
+ */
+ list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
+
+ if( count( $ext ) ) {
+ $this->mFinalExtension = trim( $ext[count( $ext ) - 1] );
+ } else {
+ $this->mFinalExtension = '';
+ }
+
+ /* Don't allow users to override the blacklist (check file extension) */
+ global $wgCheckFileExtensions, $wgStrictFileExtensions;
+ global $wgFileExtensions, $wgFileBlacklist;
+ if ( $this->mFinalExtension == '' ) {
+ $this->mTitleError = self::FILETYPE_MISSING;
+ return $this->mTitle = null;
+ } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
+ ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
+ !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
+ $this->mTitleError = self::FILETYPE_BADTYPE;
+ return $this->mTitle = null;
+ }
+
+ # If there was more than one "extension", reassemble the base
+ # filename to prevent bogus complaints about length
+ if( count( $ext ) > 1 ) {
+ for( $i = 0; $i < count( $ext ) - 1; $i++ ) {
+ $partname .= '.' . $ext[$i];
+ }
+ }
+
+ if( strlen( $partname ) < 1 ) {
+ $this->mTitleError = self::MIN_LENGTH_PARTNAME;
+ return $this->mTitle = null;
+ }
+
+ $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
+ if( is_null( $nt ) ) {
+ $this->mTitleError = self::ILLEGAL_FILENAME;
+ return $this->mTitle = null;
+ }
+ return $this->mTitle = $nt;
+ }
+
+ /**
+ * Return the local file and initializes if necessary.
+ */
+ public function getLocalFile() {
+ if( is_null( $this->mLocalFile ) ) {
+ $nt = $this->getTitle();
+ $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
+ }
+ return $this->mLocalFile;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing
+ * after the user has confirmed it.
+ *
+ * If the user doesn't explicitly cancel or accept, these files
+ * can accumulate in the temp directory.
+ *
+ * @param string $saveName - the destination filename
+ * @param string $tempSrc - the source temporary file to save
+ * @return string - full path the stashed file, or false on failure
+ */
+ protected function saveTempUploadedFile( $saveName, $tempSrc ) {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $status = $repo->storeTemp( $saveName, $tempSrc );
+ return $status;
+ }
+
+ /**
+ * Stash a file in a temporary directory for later processing,
+ * and save the necessary descriptive info into the session.
+ * Returns a key value which will be passed through a form
+ * to pick up the path info on a later invocation.
+ *
+ * @return int Session key
+ */
+ public function stashSession() {
+ $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath );
+ if( !$status->isOK() ) {
+ # Couldn't save the file.
+ return false;
+ }
+ if( !isset( $_SESSION ) ) {
+ session_start(); // start up the session (might have been previously closed to prevent php session locking)
+ }
+ $key = $this->getSessionKey();
+ $_SESSION['wsUploadData'][$key] = array(
+ 'mTempPath' => $status->value,
+ 'mFileSize' => $this->mFileSize,
+ 'mFileProps' => $this->mFileProps,
+ 'version' => self::SESSION_VERSION,
+ );
+ return $key;
+ }
+
+ /**
+ * Generate a random session key from stash in cases where we want to start an upload without much information
+ */
+ protected function getSessionKey() {
+ $key = mt_rand( 0, 0x7fffffff );
+ $_SESSION['wsUploadData'][$key] = array();
+ return $key;
+ }
+
+ /**
+ * If we've modified the upload file we need to manually remove it
+ * on exit to clean up.
+ */
+ public function cleanupTempFile() {
+ if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) {
+ wfDebug( __METHOD__ . ": Removing temporary file {$this->mTempPath}\n" );
+ unlink( $this->mTempPath );
+ }
+ }
+
+ public function getTempPath() {
+ return $this->mTempPath;
+ }
+
+ /**
+ * Split a file into a base name and all dot-delimited 'extensions'
+ * on the end. Some web server configurations will fall back to
+ * earlier pseudo-'extensions' to determine type and execute
+ * scripts, so the blacklist needs to check them all.
+ *
+ * @return array
+ */
+ public static 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
+ */
+ public static 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
+ */
+ public static function checkFileExtensionList( $ext, $list ) {
+ foreach( $ext as $e ) {
+ if( in_array( strtolower( $e ), $list ) ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks if the mime type of the uploaded file matches the file extension.
+ *
+ * @param string $mime the mime type of the uploaded file
+ * @param string $extension The filename extension that the file is to be served with
+ * @return bool
+ */
+ public static function verifyExtension( $mime, $extension ) {
+ $magic = MimeMagic::singleton();
+
+ if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' )
+ if ( !$magic->isRecognizableExtension( $extension ) ) {
+ wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
+ "unrecognized extension '$extension', can't verify\n" );
+ return true;
+ } else {
+ wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; ".
+ "recognized extension '$extension', so probably invalid file\n" );
+ return false;
+ }
+
+ $match = $magic->isMatchingExtension( $extension, $mime );
+
+ if ( $match === null ) {
+ wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file\n" );
+ return true;
+ } elseif( $match === true ) {
+ wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file\n" );
+
+ #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it!
+ return true;
+
+ } else {
+ wfDebug( __METHOD__ . ": mime type $mime mismatches file extension $extension, rejecting file\n" );
+ return false;
+ }
+ }
+
+ /**
+ * Heuristic for detecting files that *could* contain JavaScript instructions or
+ * things that may look like HTML to a browser and are thus
+ * potentially harmful. The present implementation will produce false
+ * positives in some situations.
+ *
+ * @param string $file Pathname to the temporary upload file
+ * @param string $mime The mime type of the file
+ * @param string $extension The extension of the file
+ * @return bool true if the file contains something looking like embedded scripts
+ */
+ public static function detectScript( $file, $mime, $extension ) {
+ global $wgAllowTitlesInSVG;
+
+ # ugly hack: for text files, always look at the entire file.
+ # For binary field, just check the first K.
+
+ if( strpos( $mime,'text/' ) === 0 ) {
+ $chunk = file_get_contents( $file );
+ } else {
+ $fp = fopen( $file, 'rb' );
+ $chunk = fread( $fp, 1024 );
+ fclose( $fp );
+ }
+
+ $chunk = strtolower( $chunk );
+
+ if( !$chunk ) {
+ return false;
+ }
+
+ # decode from UTF-16 if needed (could be used for obfuscation).
+ if( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
+ $enc = 'UTF-16BE';
+ } elseif( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
+ $enc = 'UTF-16LE';
+ } else {
+ $enc = null;
+ }
+
+ if( $enc ) {
+ $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
+ }
+
+ $chunk = trim( $chunk );
+
+ # FIXME: convert from UTF-16 if necessarry!
+ wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" );
+
+ # check for HTML doctype
+ if ( preg_match( "/<!DOCTYPE *X?HTML/i", $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(
+ '<a href',
+ '<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( __METHOD__ . ": no scripts found\n" );
+ return false;
+ }
+
+ protected function detectScriptInSvg( $filename ) {
+ $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) );
+ return $check->filterMatch;
+ }
+
+ /**
+ * @todo Replace this with a whitelist filter!
+ */
+ public function checkSvgScriptCallback( $element, $attribs ) {
+ $stripped = $this->stripXmlNamespace( $element );
+
+ if( $stripped == 'script' ) {
+ wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" );
+ return true;
+ }
+
+ foreach( $attribs as $attrib => $value ) {
+ $stripped = $this->stripXmlNamespace( $attrib );
+ if( substr( $stripped, 0, 2 ) == 'on' ) {
+ wfDebug( __METHOD__ . ": Found script attribute '$attrib'='value' in uploaded file.\n" );
+ return true;
+ }
+ if( $stripped == 'href' && strpos( strtolower( $value ), 'javascript:' ) !== false ) {
+ wfDebug( __METHOD__ . ": Found script href attribute '$attrib'='$value' in uploaded file.\n" );
+ return true;
+ }
+ }
+ }
+
+ private function stripXmlNamespace( $name ) {
+ // 'http://www.w3.org/2000/svg:script' -> 'script'
+ $parts = explode( ':', strtolower( $name ) );
+ return array_pop( $parts );
+ }
+
+ /**
+ * 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.
+ */
+ public static 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\">\n$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 = wfShellExec( "$command 2>&1", $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;
+ }
+ } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
+ # scan failed because filetype is unknown (probably imune)
+ wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" );
+ return null;
+ } elseif ( $mappedCode === AV_NO_VIRUS ) {
+ # no virus found
+ wfDebug( __METHOD__ . ": file passed virus scan.\n" );
+ return false;
+ } else {
+ $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 \n" );
+ 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.
+ */
+ private function checkMacBinary() {
+ $macbin = new MacBinary( $this->mTempPath );
+ if( $macbin->isValid() ) {
+ $dataFile = tempnam( wfTempDir(), 'WikiMacBinary' );
+ $dataHandle = fopen( $dataFile, 'wb' );
+
+ wfDebug( __METHOD__ . ": Extracting MacBinary data fork to $dataFile\n" );
+ $macbin->extractData( $dataHandle );
+
+ $this->mTempPath = $dataFile;
+ $this->mFileSize = $macbin->dataForkLength();
+
+ // We'll have to manually remove the new file if it's not kept.
+ $this->mRemoveTempFile = true;
+ }
+ $macbin->close();
+ }
+
+ /**
+ * Check if there's an overwrite conflict and, if so, if restrictions
+ * forbid this user from performing the upload.
+ *
+ * @return mixed true on success, error string on failure
+ */
+ private function checkOverwrite() {
+ global $wgUser;
+ // First check whether the local file can be overwritten
+ $file = $this->getLocalFile();
+ if( $file->exists() ) {
+ if( !self::userCanReUpload( $wgUser, $file ) ) {
+ return 'fileexists-forbidden';
+ } else {
+ return true;
+ }
+ }
+
+ /* Check shared conflicts: if the local file does not exist, but
+ * wfFindFile finds a file, it exists in a shared repository.
+ */
+ $file = wfFindFile( $this->getTitle() );
+ if ( $file && !$wgUser->isAllowed( 'reupload-shared' ) ) {
+ return 'fileexists-shared-forbidden';
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a user is the last uploader
+ *
+ * @param User $user
+ * @param string $img, image name
+ * @return bool
+ */
+ public static function userCanReUpload( User $user, $img ) {
+ if( $user->isAllowed( 'reupload' ) ) {
+ return true; // non-conditional
+ }
+ if( !$user->isAllowed( 'reupload-own' ) ) {
+ return false;
+ }
+ if( is_string( $img ) ) {
+ $img = wfLocalFile( $img );
+ }
+ if ( !( $img instanceof LocalFile ) ) {
+ return false;
+ }
+
+ return $user->getId() == $img->getUser( 'id' );
+ }
+
+ /**
+ * Helper function that does various existence checks for a file.
+ * The following checks are performed:
+ * - The file exists
+ * - Article with the same name as the file exists
+ * - File exists with normalized extension
+ * - The file looks like a thumbnail and the original exists
+ *
+ * @param File $file The file to check
+ * @return mixed False if the file does not exists, else an array
+ */
+ public static function getExistsWarning( $file ) {
+ if( $file->exists() ) {
+ return array( 'warning' => 'exists', 'file' => $file );
+ }
+
+ if( $file->getTitle()->getArticleID() ) {
+ return array( 'warning' => 'page-exists', 'file' => $file );
+ }
+
+ if ( $file->wasDeleted() && !$file->exists() ) {
+ return array( 'warning' => 'was-deleted', 'file' => $file );
+ }
+
+ if( strpos( $file->getName(), '.' ) == false ) {
+ $partname = $file->getName();
+ $extension = '';
+ } else {
+ $n = strrpos( $file->getName(), '.' );
+ $extension = substr( $file->getName(), $n + 1 );
+ $partname = substr( $file->getName(), 0, $n );
+ }
+ $normalizedExtension = File::normalizeExtension( $extension );
+
+ if ( $normalizedExtension != $extension ) {
+ // We're not using the normalized form of the extension.
+ // Normal form is lowercase, using most common of alternate
+ // extensions (eg 'jpg' rather than 'JPEG').
+ //
+ // Check for another file using the normalized form...
+ $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
+ $file_lc = wfLocalFile( $nt_lc );
+
+ if( $file_lc->exists() ) {
+ return array(
+ 'warning' => 'exists-normalized',
+ 'file' => $file,
+ 'normalizedFile' => $file_lc
+ );
+ }
+ }
+
+ if ( self::isThumbName( $file->getName() ) ) {
+ # Check for filenames like 50px- or 180px-, these are mostly thumbnails
+ $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $extension, NS_FILE );
+ $file_thb = wfLocalFile( $nt_thb );
+ if( $file_thb->exists() ) {
+ return array(
+ 'warning' => 'thumb',
+ 'file' => $file,
+ 'thumbFile' => $file_thb
+ );
+ } else {
+ // File does not exist, but we just don't like the name
+ return array(
+ 'warning' => 'thumb-name',
+ 'file' => $file,
+ 'thumbFile' => $file_thb
+ );
+ }
+ }
+
+
+ foreach( self::getFilenamePrefixBlacklist() as $prefix ) {
+ if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
+ return array(
+ 'warning' => 'bad-prefix',
+ 'file' => $file,
+ 'prefix' => $prefix
+ );
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Helper function that checks whether the filename looks like a thumbnail
+ */
+ public static function isThumbName( $filename ) {
+ $n = strrpos( $filename, '.' );
+ $partname = $n ? substr( $filename, 0, $n ) : $filename;
+ return (
+ substr( $partname , 3, 3 ) == 'px-' ||
+ substr( $partname , 2, 3 ) == 'px-'
+ ) &&
+ preg_match( "/[0-9]{2}/" , substr( $partname , 0, 2 ) );
+ }
+
+ /**
+ * Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]]
+ *
+ * @return array list of prefixes
+ */
+ public static function getFilenamePrefixBlacklist() {
+ $blacklist = array();
+ $message = wfMsgForContent( 'filename-prefix-blacklist' );
+ if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) {
+ $lines = explode( "\n", $message );
+ foreach( $lines as $line ) {
+ // Remove comment lines
+ $comment = substr( trim( $line ), 0, 1 );
+ if ( $comment == '#' || $comment == '' ) {
+ continue;
+ }
+ // Remove additional comments after a prefix
+ $comment = strpos( $line, '#' );
+ if ( $comment > 0 ) {
+ $line = substr( $line, 0, $comment-1 );
+ }
+ $blacklist[] = trim( $line );
+ }
+ }
+ return $blacklist;
+ }
+
+ public function getImageInfo( $result ) {
+ $file = $this->getLocalFile();
+ $imParam = ApiQueryImageInfo::getPropertyNames();
+ return ApiQueryImageInfo::getInfo( $file, array_flip( $imParam ), $result );
+ }
+
+}
diff --git a/includes/upload/UploadFromFile.php b/includes/upload/UploadFromFile.php
new file mode 100644
index 00000000..73581a61
--- /dev/null
+++ b/includes/upload/UploadFromFile.php
@@ -0,0 +1,32 @@
+<?php
+/**
+ * @file
+ * @ingroup upload
+ *
+ * @author Bryan Tong Minh
+ *
+ * Implements regular file uploads
+ */
+class UploadFromFile extends UploadBase {
+
+
+ function initializeFromRequest( &$request ) {
+ $desiredDestName = $request->getText( 'wpDestFile' );
+ if( !$desiredDestName )
+ $desiredDestName = $request->getFileName( 'wpUploadFile' );
+ return $this->initializePathInfo(
+ $desiredDestName,
+ $request->getFileTempName( 'wpUploadFile' ),
+ $request->getFileSize( 'wpUploadFile' )
+ );
+ }
+ /**
+ * Entry point for upload from file.
+ */
+ function initialize( $name, $tempPath, $fileSize ) {
+ return $this->initializePathInfo( $name, $tempPath, $fileSize );
+ }
+ static function isValidRequest( $request ) {
+ return (bool)$request->getFileTempName( 'wpUploadFile' );
+ }
+}
diff --git a/includes/upload/UploadFromStash.php b/includes/upload/UploadFromStash.php
new file mode 100644
index 00000000..17e922b0
--- /dev/null
+++ b/includes/upload/UploadFromStash.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * @file
+ * @ingroup upload
+ *
+ * Implements uploading from previously stored file.
+ *
+ * @author Bryan Tong Minh
+ */
+
+class UploadFromStash extends UploadBase {
+ public static function isValidSessionKey( $key, $sessionData ) {
+ return !empty( $key ) &&
+ is_array( $sessionData ) &&
+ isset( $sessionData[$key] ) &&
+ isset( $sessionData[$key]['version'] ) &&
+ $sessionData[$key]['version'] == self::SESSION_VERSION;
+ }
+
+ public static function isValidRequest( $request ) {
+ $sessionData = $request->getSessionData( 'wsUploadData' );
+ return self::isValidSessionKey(
+ $request->getInt( 'wpSessionKey' ),
+ $sessionData
+ );
+ }
+
+ public function initialize( $name, $sessionKey, $sessionData ) {
+ /**
+ * Confirming a temporarily stashed upload.
+ * We don't want path names to be forged, so we keep
+ * them in the session on the server and just give
+ * an opaque key to the user agent.
+ */
+
+ $this->initializePathInfo( $name,
+ $this->getRealPath ( $sessionData['mTempPath'] ),
+ $sessionData['mFileSize'],
+ false
+ );
+
+ $this->mSessionKey = $sessionKey;
+ $this->mVirtualTempPath = $sessionData['mTempPath'];
+ $this->mFileProps = $sessionData['mFileProps'];
+ }
+
+ public function initializeFromRequest( &$request ) {
+ $sessionKey = $request->getInt( 'wpSessionKey' );
+ $sessionData = $request->getSessionData('wsUploadData');
+
+ $desiredDestName = $request->getText( 'wpDestFile' );
+ if( !$desiredDestName )
+ $desiredDestName = $request->getText( 'wpUploadFile' );
+ return $this->initialize( $desiredDestName, $sessionKey, $sessionData[$sessionKey] );
+ }
+
+ /**
+ * File has been previously verified so no need to do so again.
+ */
+ protected function verifyFile() {
+ return true;
+ }
+
+
+ /**
+ * There is no need to stash the image twice
+ */
+ public function stashSession() {
+ if ( !empty( $this->mSessionKey ) )
+ return $this->mSessionKey;
+ return parent::stashSession();
+ }
+
+ /**
+ * Remove a temporarily kept file stashed by saveTempUploadedFile().
+ * @return success
+ */
+ public function unsaveUploadedFile() {
+ $repo = RepoGroup::singleton()->getLocalRepo();
+ $success = $repo->freeTemp( $this->mVirtualTempPath );
+ return $success;
+ }
+
+} \ No newline at end of file
diff --git a/includes/upload/UploadFromUrl.php b/includes/upload/UploadFromUrl.php
new file mode 100644
index 00000000..763dae38
--- /dev/null
+++ b/includes/upload/UploadFromUrl.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * @file
+ * @ingroup upload
+ *
+ * Implements uploading from a HTTP resource.
+ *
+ * @author Bryan Tong Minh
+ * @author Michael Dale
+ */
+class UploadFromUrl extends UploadBase {
+ protected $mTempDownloadPath;
+
+ /**
+ * Checks if the user is allowed to use the upload-by-URL feature. If the
+ * user is allowed, pass on permissions checking to the parent.
+ */
+ public static function isAllowed( $user ) {
+ if( !$user->isAllowed( 'upload_by_url' ) )
+ return 'upload_by_url';
+ return parent::isAllowed( $user );
+ }
+
+ /**
+ * Checks if the upload from URL feature is enabled
+ */
+ public static function isEnabled() {
+ global $wgAllowCopyUploads;
+ return $wgAllowCopyUploads && parent::isEnabled();
+ }
+
+ /**
+ * Entry point for API upload
+ */
+ public function initialize( $name, $url, $na, $nb = false ) {
+ global $wgTmpDirectory;
+
+ $localFile = tempnam( $wgTmpDirectory, 'WEBUPLOAD' );
+ $this->initializePathInfo( $name, $localFile, 0, true );
+
+ $this->mUrl = trim( $url );
+ }
+
+ /**
+ * Entry point for SpecialUpload
+ * @param $request Object: WebRequest object
+ */
+ public function initializeFromRequest( &$request ) {
+ $desiredDestName = $request->getText( 'wpDestFile' );
+ if( !$desiredDestName )
+ $desiredDestName = $request->getText( 'wpUploadFileURL' );
+ return $this->initialize(
+ $desiredDestName,
+ $request->getVal( 'wpUploadFileURL' ),
+ false
+ );
+ }
+
+ /**
+ * @param $request Object: WebRequest object
+ */
+ public static function isValidRequest( $request ){
+ if( !$request->getVal( 'wpUploadFileURL' ) )
+ return false;
+ // check that is a valid url:
+ return self::isValidUrl( $request->getVal( 'wpUploadFileURL' ) );
+ }
+
+ public static function isValidUrl( $url ) {
+ // Only allow HTTP or FTP for now
+ return (bool)preg_match( '!^(http://|ftp://)!', $url );
+ }
+
+ /**
+ * Do the real fetching stuff
+ */
+ function fetchFile() {
+ if( !self::isValidUrl( $this->mUrl ) ) {
+ return Status::newFatal( 'upload-proto-error' );
+ }
+ $res = $this->curlCopy();
+ if( $res !== true ) {
+ return Status::newFatal( $res );
+ }
+ return Status::newGood();
+ }
+
+ /**
+ * Safe copy from URL
+ * Returns true if there was an error, false otherwise
+ */
+ private function curlCopy() {
+ global $wgOut;
+
+ # Open temporary file
+ $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" );
+ if( $this->mCurlDestHandle === false ) {
+ # Could not open temporary file to write in
+ return 'upload-file-error';
+ }
+
+ $ch = curl_init();
+ curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug
+ curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout
+ curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed
+ curl_setopt( $ch, CURLOPT_URL, $this->mUrl);
+ curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) );
+ curl_exec( $ch );
+ $error = curl_errno( $ch );
+ curl_close( $ch );
+
+ fclose( $this->mCurlDestHandle );
+ unset( $this->mCurlDestHandle );
+
+ if( $error )
+ return "upload-curl-error$errornum";
+
+ return true;
+ }
+
+ /**
+ * Callback function for CURL-based web transfer
+ * Write data to file unless we've passed the length limit;
+ * if so, abort immediately.
+ * @access private
+ */
+ function uploadCurlCallback( $ch, $data ) {
+ global $wgMaxUploadSize;
+ $length = strlen( $data );
+ $this->mFileSize += $length;
+ if( $this->mFileSize > $wgMaxUploadSize ) {
+ return 0;
+ }
+ fwrite( $this->mCurlDestHandle, $data );
+ return $length;
+ }
+}
diff --git a/includes/zhtable/Makefile b/includes/zhtable/Makefile
index 618e2f21..5dd88d38 100644
--- a/includes/zhtable/Makefile
+++ b/includes/zhtable/Makefile
@@ -23,7 +23,7 @@ all: ZhConversion.php tradphrases.notsure simpphrases.notsure wordlist toHans.di
# Download Unihan database and Traditional Chinese / Simplified Chinese phrases files
Unihan.zip:
- wget -nc ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip
+ wget -nc http://www.unicode.org/Public/UNIDATA/Unihan.zip
scim-tables-$(SCIM_TABLES_VER).tar.gz:
wget -nc http://$(SF_MIRROR).dl.sourceforge.net/sourceforge/scim/scim-tables-$(SCIM_TABLES_VER).tar.gz
diff --git a/includes/zhtable/Makefile.py b/includes/zhtable/Makefile.py
index 19436457..26e229df 100644
--- a/includes/zhtable/Makefile.py
+++ b/includes/zhtable/Makefile.py
@@ -1,7 +1,25 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
# @author Philip
-# You should run this script UNDER python 3000.
import tarfile, zipfile
-import os, re, shutil, urllib.request
+import os, re, shutil, sys, platform
+
+pyversion = platform.python_version()
+islinux = platform.system().lower() == 'linux' or False
+
+if pyversion[:3] in ['2.5', '2.6', '2.7']:
+ import urllib as urllib_request
+ import codecs
+ uniopen = codecs.open
+ def unichr2(i):
+ if sys.maxunicode >= 0x10000 or i < 0x10000:
+ return unichr(i)
+ else:
+ return unichr(0xD7C0+(i>>10)) + unichr(0xDC00+(i&0x3FF))
+elif pyversion[:2] == '3.':
+ import urllib.request as urllib_request
+ uniopen = open
+ unichr2 = chr
# DEFINE
SF_MIRROR = 'easynews'
@@ -14,14 +32,23 @@ def GetFileFromURL( url, dest ):
if os.path.isfile(dest):
print( 'File %s up to date.' % dest )
return
- print( 'Downloading from [%s] ...' % url )
- urllib.request.urlretrieve( url, dest )
- print( 'Download complete.\n' )
+ global islinux
+ if islinux:
+ # we use wget instead urlretrieve under Linux,
+ # because wget will display details like download progress
+ os.system('wget %s' % url)
+ else:
+ print( 'Downloading from [%s] ...' % url )
+ urllib_request.urlretrieve( url, dest )
+ print( 'Download complete.\n' )
return
-def GetFileFromZip( path ):
+def GetFileFromUnihan( path ):
print( 'Extracting files from %s ...' % path )
- zipfile.ZipFile(path).extractall()
+ text = zipfile.ZipFile(path).read('Unihan_Variants.txt')
+ uhfile = uniopen('Unihan_Variants.txt', 'w')
+ uhfile.write(text)
+ uhfile.close()
return
def GetFileFromTar( path, member, rename ):
@@ -34,25 +61,25 @@ def GetFileFromTar( path, member, rename ):
def ReadBIG5File( dest ):
print( 'Reading and decoding %s ...' % dest )
- f1 = open( dest, 'r', encoding='big5hkscs', errors='replace' )
+ f1 = uniopen( dest, 'r', encoding='big5hkscs', errors='replace' )
text = f1.read()
text = text.replace( '\ufffd', '\n' )
f1.close()
- f2 = open( dest, 'w', encoding='utf8' )
+ f2 = uniopen( dest, 'w', encoding='utf8' )
f2.write(text)
f2.close()
return text
def ReadFile( dest ):
print( 'Reading and decoding %s ...' % dest )
- f = open( dest, 'r', encoding='utf8' )
+ f = uniopen( dest, 'r', encoding='utf8' )
ret = f.read()
f.close()
return ret
def ReadUnihanFile( dest ):
print( 'Reading and decoding %s ...' % dest )
- f = open( dest, 'r', encoding='utf8' )
+ f = uniopen( dest, 'r', encoding='utf8' )
t2s_code = []
s2t_code = []
while True:
@@ -82,7 +109,7 @@ def RemoveOneCharConv( text ):
def ConvertToChar( code ):
code = code.split('<')[0]
- return chr( int( code[2:], 16 ) )
+ return unichr2( int( code[2:], 16 ) )
def GetDefaultTable( code_table ):
char_table = {}
@@ -101,8 +128,8 @@ def GetManualTable( dest ):
elem = elem.strip('|')
if elem:
temp2 = elem.split( '|', 1 )
- from_char = chr( int( temp2[0][2:7], 16 ) )
- to_chars = [chr( int( code[2:7], 16 ) ) for code in temp2[1].split('|')]
+ from_char = unichr2( int( temp2[0][2:7], 16 ) )
+ to_chars = [unichr2( int( code[2:7], 16 ) ) for code in temp2[1].split('|')]
char_table[from_char] = to_chars
return char_table
@@ -222,24 +249,26 @@ def GetManualWordsTable( src_wordlist, conv_table ):
def CustomRules( dest ):
text = ReadFile( dest )
temp = text.split()
- ret = {temp[i]: temp[i + 1] for i in range( 0, len( temp ), 2 )}
+ ret = dict()
+ for i in range( 0, len( temp ), 2 ):
+ ret[temp[i]] = temp[i + 1]
return ret
def GetPHPArray( table ):
- lines = ['\'%s\' => \'%s\',' % (f, t) for (f, t) in table]
+ lines = ['\'%s\' => \'%s\',' % (f, t) for (f, t) in table if f and t]
#lines = ['"%s"=>"%s",' % (f, t) for (f, t) in table]
return '\n'.join(lines)
def RemoveSameChar( src_table ):
dst_table = {}
for f, t in src_table.items():
- if not f == t:
+ if f != t:
dst_table[f] = t
return dst_table
def main():
#Get Unihan.zip:
- url = 'ftp://ftp.unicode.org/Public/UNIDATA/Unihan.zip'
+ url = 'http://www.unicode.org/Public/UNIDATA/Unihan.zip'
han_dest = 'Unihan.zip'
GetFileFromURL( url, han_dest )
@@ -261,7 +290,7 @@ def main():
# Extract the file from a comressed files
# Unihan.txt Simp. & Trad
- GetFileFromZip( han_dest )
+ GetFileFromUnihan( han_dest )
# Make word lists
t_wordlist = []
@@ -341,7 +370,7 @@ def main():
# Make char to char convertion table
# Unihan.txt, dict t2s_code, s2t_code = { 'U+XXXX': 'U+YYYY( U+ZZZZ) ... ', ... }
- ( t2s_code, s2t_code ) = ReadUnihanFile( 'Unihan.txt' )
+ ( t2s_code, s2t_code ) = ReadUnihanFile( 'Unihan_Variants.txt' )
# dict t2s_1tomany = { '\uXXXX': '\uYYYY\uZZZZ ... ', ... }
t2s_1tomany = {}
t2s_1tomany.update( GetDefaultTable( t2s_code ) )
@@ -429,10 +458,20 @@ $zh2Hant = array(\n'''
php += GetPHPArray( toSG )
php += '\n);'
- f = open( 'ZhConversion.php', 'w', encoding = 'utf8' )
+ f = uniopen( 'ZhConversion.php', 'w', encoding = 'utf8' )
print ('Writing ZhConversion.php ... ')
f.write( php )
f.close()
+
+ #Remove temp files
+ print ('Deleting temp files ... ')
+ os.remove('EZ.txt.in')
+ os.remove('phrase_lib.txt')
+ os.remove('tsi.src')
+ os.remove('Unihan_Variants.txt')
+ os.remove('Wubi.txt.in')
+ os.remove('Ziranma.txt.in')
+
if __name__ == '__main__':
- main() \ No newline at end of file
+ main()
diff --git a/includes/zhtable/simp2trad.manual b/includes/zhtable/simp2trad.manual
index 2a405073..bb4eb7ef 100644
--- a/includes/zhtable/simp2trad.manual
+++ b/includes/zhtable/simp2trad.manual
@@ -1,215 +1,5 @@
-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+04e11丑|U+0919c醜|
-U+051fa出|U+051fa出|U+09f63齣|
-U+06dc0淀|U+06dc0淀|U+06fb1澱|
-U+051ac冬|U+051ac冬|U+09f15鼕|
-U+08303范|U+08303范|U+07bc4範|
-U+04e30丰|U+08c50豐|U+04e30丰|
-U+0522e刮|U+0522e刮|U+098b3颳|
-U+0540e后|U+0540e后|U+05f8c後|
-U+080e1胡|U+080e1胡|U+09b0d鬍|U+0885a衚|
-U+056de回|U+056de回|U+08ff4迴|
-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睏|
-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+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+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云|
-U+082b8芸|U+082b8芸|U+08553蕓|
-U+06c84沄|U+06c84沄|U+06f90澐|
-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+051c6准|U+06e96準|
-U+05382厂|U+05ee0廠|U+05382厂|
-U+05e7f广|U+05ee3廣|U+05e7f广|
-U+08f9f辟|U+08f9f辟|U+095e2闢|
-U+0522b别|U+05225別|U+05f46彆|
-U+0535c卜|U+0535c卜|U+08514蔔|
-U+06c88沈|U+06c88沈|U+0700b瀋|
-U+051b2冲|U+06c96沖|U+0885d衝|
-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+06597斗|U+09b25鬥|
-U+0513f儿|U+05152兒|U+0513f儿|
-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+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+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+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+06734朴|U+06a38樸|
-U+082f9苹|U+0860b蘋|U+082f9苹|
-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+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+04f63佣|U+050ad傭|
-U+04e0e与|U+08207與|U+04e0e与|
-U+06298折|U+06298折|U+0647a摺|
-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+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+053a4厤|
-U+05364卤|U+09e75鹵|U+06ef7滷|
-U+05f25弥|U+05f4c彌|U+07030瀰|
-U+07b7e签|U+07c3d簽|U+07c64籤|
-U+07ea4纤|U+07e96纖|U+07e34縴|
-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+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+0540a吊|U+05f14弔|
-U+04ec7仇|U+04ec7仇|U+08b8e讎|
-U+08574蕴|U+0860a蘊|U+085f4藴|
-U+07ebf线|U+07dda線|U+07dab綫|
-U+04e3a为|U+070ba為|U+07232爲|
-U+04ea7产|U+07522產|U+07523産|
-U+04f17众|U+0773e眾|U+08846衆|
-U+04f2a伪|U+0507d偽|U+050de僞|
-U+051eb凫|U+09ce7鳧|U+09cec鳬|
-U+05395厕|U+05ec1廁|U+053a0厠|
-U+0542f启|U+0555f啟|U+05553啓|
-U+05899墙|U+07246牆|U+058bb墻|
-U+058f3壳|U+06bbc殼|U+06bbb殻|
-U+05956奖|U+0734e獎|U+0596c奬|
-U+059ab妫|U+05aaf媯|U+05b00嬀|
-U+05e76并|U+04e26並|U+04f75併|
-U+05f55录|U+09304錄|U+09332録|
-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+07906礆|U+09e7c鹼|
-U+07ad6竖|U+08c4e豎|U+07aea竪|
-U+07edd绝|U+07d55絕|U+07d76絶|
-U+07ee3绣|U+07d89綉|U+07e61繡|
-U+07ee6绦|U+07d5b絛|U+07e27縧|
-U+07ef1绱|U+07dd4緔|U+0979d鞝|
-U+07ef7绷|U+07db3綳|U+07e43繃|
-U+07eff绿|U+07da0綠|U+07dd1緑|
-U+07f30缰|U+097c1韁|U+07e6e繮|
-U+082e7苧|U+082ce苎|U+085b4薴|
-U+083bc莼|U+08493蒓|U+084f4蓴|
-U+08bf4说|U+08aaa說|U+08aac説|
-U+08c23谣|U+08b20謠|U+08b21謡|
-U+08c2b谫|U+08b7e譾|U+08b2d謭|
-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+091c7采|U+063a1採|U+057f0埰|
-U+094a9钩|U+09264鉤|U+0920e鈎|
-U+094b5钵|U+07f3d缽|U+09262鉢|
-U+09508锈|U+092b9銹|U+093fd鏽|
-U+09510锐|U+092b3銳|U+092ed鋭|
-U+09528锨|U+06774杴|U+09341鍁|
-U+0954c镌|U+0942b鐫|U+093b8鎸|
-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+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讚|
-U+0E82D|U+068E1棡|
-U+29F8C𩾌|U+09C47鱇|
-U+070BC炼|U+07149煉|U+0934A鍊|
-U+04E2A个|U+0500B個|U+07B87箇|
-U+094F2铲|U+093DF鏟|U+05277剷|
-U+05EB5庵|U+05EB5庵|U+083F4菴|
-U+05F69彩|U+05F69彩|U+07DB5綵|
-U+20BB6𠮶|U+055F0嗰|
+U+03CE0㳠|U+06FBE澾|
+U+0447D䑽|U+26A99𦪙|
U+0497A䥺|U+091FE釾|
U+0497D䥽|U+093FA鏺|
U+04983䦃|U+0942F鐯|
@@ -220,6 +10,237 @@ U+04CA0䲠|U+09C06鰆|
U+04CA1䲡|U+09C0C鰌|
U+04CA2䲢|U+09C27鰧|
U+04CA3䲣|U+04C77䱷|
+U+04E07万|U+0842C萬|U+04E07万|
+U+04E0E与|U+08207與|U+04E0E与|
+U+04E11丑|U+04E11丑|U+0919C醜|
+U+04E2A个|U+0500B個|U+07B87箇|
+U+04E30丰|U+08C50豐|U+04E30丰|
+U+04E3A为|U+070BA為|U+07232爲|
+U+04E48么|U+04E48么|U+09EBD麽|U+05E7A幺|U+09EBC麼|
+U+04E86了|U+04E86了|U+077AD瞭|
+U+04E8E于|U+065BC於|U+04E8E于|
+U+04E91云|U+096F2雲|U+04E91云|
+U+04EA7产|U+07522產|U+07523産|
+U+04EC6仆|U+04EC6仆|U+050D5僕|
+U+04EC7仇|U+04EC7仇|U+08B8E讎|
+U+04ED1仑|U+04F96侖|U+05D19崙|
+U+04EF7价|U+050F9價|U+04EF7价|
+U+04F17众|U+0773E眾|U+08846衆|
+U+04F19伙|U+04F19伙|U+05925夥|
+U+04F2A伪|U+0507D偽|U+050DE僞|
+U+04F53体|U+09AD4體|U+04F53体|
+U+04F59余|U+04F59余|U+09918餘|
+U+04F63佣|U+04F63佣|U+050AD傭|
+U+0501F借|U+0501F借|U+085C9藉|
+U+0513F儿|U+05152兒|U+0513F儿|
+U+0514B克|U+0514B克|U+0524B剋|
+U+0515A党|U+09EE8黨|U+0515A党|
+U+051AC冬|U+051AC冬|U+09F15鼕|
+U+051B2冲|U+06C96沖|U+0885D衝|
+U+051C6准|U+051C6准|U+06E96準|
+U+051E0几|U+05E7E幾|U+051E0几|
+U+051EB凫|U+09CE7鳧|U+09CEC鳬|
+U+051FA出|U+051FA出|U+09F63齣|
+U+05212划|U+05212划|U+05283劃|
+U+0522B别|U+05225別|U+05F46彆|
+U+0522E刮|U+0522E刮|U+098B3颳|
+U+05236制|U+05236制|U+088FD製|
+U+05343千|U+05343千|U+097C6韆|
+U+05347升|U+05347升|U+06607昇|U+0965E陞|
+U+0535C卜|U+0535C卜|U+08514蔔|
+U+05360占|U+05360占|U+04F54佔|
+U+05364卤|U+09E75鹵|U+06EF7滷|
+U+05377卷|U+05377卷|U+06372捲|
+U+0537A卺|U+05DF9巹|
+U+05382厂|U+05EE0廠|U+05382厂|
+U+05386历|U+06B77歷|U+066C6曆|U+053A4厤|
+U+05395厕|U+05EC1廁|U+053A0厠|
+U+05398厘|U+05398厘|U+091D0釐|
+U+053D1发|U+0767C發|U+09AEE髮|
+U+053EA只|U+053EA只|U+096BB隻|
+U+053F0台|U+053F0台|U+081FA臺|U+06AAF檯|U+098B1颱|
+U+053F6叶|U+08449葉|U+053F6叶|
+U+05401吁|U+05401吁|U+07C72籲|
+U+05408合|U+05408合|U+095A4閤|
+U+0540A吊|U+0540A吊|U+05F14弔|
+U+0540C同|U+0540C同|U+08855衕|
+U+0540E后|U+0540E后|U+05F8C後|
+U+05411向|U+05411向|U+056AE嚮|U+066CF曏|
+U+0542F启|U+0555F啟|U+05553啓|
+U+05446呆|U+05446呆|U+07343獃|
+U+054B8咸|U+054B8咸|U+09E79鹹|
+U+054C4哄|U+054C4哄|U+09B28鬨|
+U+05582喂|U+05582喂|U+09935餵|
+U+056DE回|U+056DE回|U+08FF4迴|
+U+056E2团|U+05718團|U+07CF0糰|
+U+056F0困|U+056F0困|U+0774F睏|
+U+05742坂|U+05742坂|U+0962A阪|
+U+0574F坏|U+058DE壞|U+0574F坏|
+U+0575B坛|U+058C7壇|U+07F48罈|
+U+057FC埼|U+057FC埼|U+07895碕|
+U+05899墙|U+07246牆|U+058BB墻|
+U+058F3壳|U+06BBC殼|U+06BBB殻|
+U+0590D复|U+05FA9復|U+08907複|
+U+05956奖|U+0734E獎|U+0596C奬|
+U+05978奸|U+05978奸|U+059E6姦|
+U+059AB妫|U+05AAF媯|U+05B00嬀|
+U+059DC姜|U+059DC姜|U+08591薑|
+U+05B81宁|U+05BE7寧|U+05B81宁|
+U+05BB6家|U+05BB6家|U+050A2傢|
+U+05C3D尽|U+076E1盡|U+05118儘|
+U+05CB3岳|U+05CB3岳|U+05DBD嶽|
+U+05E03布|U+05E03布|U+04F48佈|
+U+05E18帘|U+07C3E簾|U+05E18帘|
+U+05E5E幞|U+08946襆|
+U+05E72干|U+05E72干|U+04E7E乾|U+05E79幹|U+069A6榦|
+U+05E76并|U+04E26並|U+04F75併|
+U+05E78幸|U+05E78幸|U+05016倖|
+U+05E7F广|U+05EE3廣|U+05E7F广|
+U+05E84庄|U+05E84庄|U+0838A莊|
+U+05EB5庵|U+05EB5庵|U+083F4菴|
+U+05F25弥|U+05F4C彌|U+07030瀰|
+U+05F53当|U+07576當|U+05679噹|
+U+05F55录|U+09304錄|U+09332録|
+U+05F69彩|U+05F69彩|U+07DB5綵|
+U+05F81征|U+05F81征|U+05FB5徵|
+U+05FA1御|U+05FA1御|U+079A6禦|
+U+05FD7志|U+05FD7志|U+08A8C誌|
+U+06076恶|U+060E1惡|U+05641噁|
+U+060AB悫|U+06128愨|U+06164慤|
+U+0613F愿|U+09858願|U+0613F愿|
+U+0621A戚|U+0621A戚|U+0617C慼|U+093DA鏚|
+U+0624D才|U+0624D才|U+07E94纔|
+U+0624E扎|U+0624E扎|U+07D2E紮|
+U+06258托|U+06258托|U+08A17託|
+U+06298折|U+06298折|U+0647A摺|
+U+062C5担|U+064D4擔|U+062C5担|
+U+062FC拼|U+062FC拼|U+062DA拚|
+U+06328挨|U+06328挨|U+06371捱|
+U+0633D挽|U+0633D挽|U+08F13輓|
+U+0636E据|U+064DA據|U+0636E据|
+U+06597斗|U+06597斗|U+09B25鬥|
+U+065CB旋|U+065CB旋|U+0955F镟|
+U+065D7旗|U+065D7旗|U+065C2旂|
+U+066F2曲|U+066F2曲|U+09EAF麯|U+09EB4麯|
+U+0672F术|U+08853術|U+0672E朮|
+U+06731朱|U+06731朱|U+07843硃|
+U+06734朴|U+06734朴|U+06A38樸|
+U+0676F杯|U+0676F杯|U+076C3盃|
+U+0677E松|U+0677E松|U+09B06鬆|
+U+0677F板|U+0677F板|U+095C6闆|
+U+06781极|U+06975極|U+06781极|
+U+067DC柜|U+06AC3櫃|U+067DC柜|
+U+06817栗|U+06817栗|U+06144慄|
+U+06881梁|U+06881梁|U+06A11樑|
+U+068F1棱|U+068F1棱|U+07A1C稜|
+U+06B32欲|U+06B32欲|U+0617E慾|
+U+06C47汇|U+0532F匯|U+06ED9滙|U+05F59彙|
+U+06C84沄|U+06C84沄|U+06F90澐|
+U+06C88沈|U+06C88沈|U+0700B瀋|
+U+06CA9沩|U+06E88溈|U+06F59潙|
+U+06CE8注|U+06CE8注|U+08A3B註|
+U+06D82涂|U+05857塗|U+06D82涂|
+U+06D8C涌|U+06D8C涌|U+06E67湧|
+U+06DC0淀|U+06DC0淀|U+06FB1澱|
+U+06E38游|U+06E38游|U+0904A遊|
+U+06EAF溯|U+06EAF溯|U+06CDD泝|
+U+06F13漓|U+06F13漓|U+07055灕|
+U+070BC炼|U+07149煉|U+0934A鍊|
+U+0753B画|U+0756B畫|U+07575畵|
+U+075C7症|U+075C7症|U+07665癥|
+U+07618瘘|U+0763A瘺|U+0763B瘻|
+U+0786E确|U+078BA確|U+0786E确|
+U+07877硷|U+07906礆|U+09E7C鹼|
+U+079CB秋|U+079CB秋|U+097A6鞦|
+U+079CD种|U+07A2E種|U+079CD种|
+U+07A57穗|U+07A57穗|U+07E50繐|
+U+07AD6竖|U+08C4E豎|U+07AEA竪|
+U+07B51筑|U+07BC9築|U+07B51筑|
+U+07B7E签|U+07C3D簽|U+07C64籤|
+U+07CFB系|U+07CFB系|U+07E6B繫|U+04FC2係|
+U+07D2F累|U+07D2F累|U+07E8D纍|
+U+07EA4纤|U+07E96纖|U+07E34縴|
+U+07EBF线|U+07DDA線|U+07DAB綫|
+U+07EDD绝|U+07D55絕|U+07D76絶|
+U+07EE3绣|U+07D89綉|U+07E61繡|
+U+07EE6绦|U+07D5B絛|U+07E27縧|
+U+07EF1绱|U+07DD4緔|U+0979D鞝|
+U+07EF7绷|U+07DB3綳|U+07E43繃|
+U+07EFF绿|U+07DA0綠|U+07DD1緑|
+U+07F30缰|U+097C1韁|U+07E6E繮|
+U+07FA1羡|U+07FA8羨|
+U+080DC胜|U+052DD勝|U+080DC胜|
+U+080E1胡|U+080E1胡|U+09B0D鬍|U+0885A衚|
+U+0810F脏|U+081DF臟|U+09AD2髒|
+U+0814A腊|U+081D8臘|U+0814A腊|
+U+081F4致|U+081F4致|U+07DFB緻|
+U+0820D舍|U+0820D舍|U+06368捨|
+U+082B8芸|U+082B8芸|U+08553蕓|
+U+082CE苎|U+082E7苧|
+U+082CF苏|U+08607蘇|U+056CC囌|U+07C64甦|
+U+082E7苧|U+085B4薴|
+U+082F9苹|U+0860B蘋|U+082F9苹|
+U+08303范|U+08303范|U+07BC4範|
+U+0836F药|U+0846F葯|U+085E5藥|
+U+083B7获|U+07372獲|U+07A6B穫|
+U+083BC莼|U+08493蒓|U+084F4蓴|
+U+08499蒙|U+08499蒙|U+077C7矇|U+06FDB濛|U+061DE懞|
+U+084D1蓑|U+084D1蓑|U+07C11簑|
+U+08511蔑|U+08511蔑|U+0884A衊|
+U+08574蕴|U+0860A蘊|U+085F4藴|
+U+0866B虫|U+087F2蟲|U+0866B虫|
+U+08721蜡|U+0881F蠟|U+08721蜡|
+U+0874E蝎|U+0880D蠍|
+U+08868表|U+08868表|U+09336錶|
+U+08BF4说|U+08AAA說|U+08AAC説|
+U+08C23谣|U+08B20謠|U+08B21謡|
+U+08C2B谫|U+08B7E譾|U+08B2D謭|
+U+08C37谷|U+08C37谷|U+07A40穀|
+U+08D43赃|U+08D13贓|U+08D1C贜|
+U+08D4D赍|U+09F4E齎|U+08CEB賫|
+U+08D5D赝|U+08D17贗|U+08D0B贋|
+U+08D5E赞|U+08D0A贊|U+08B9A讚|
+U+08F9F辟|U+08F9F辟|U+095E2闢|
+U+09002适|U+09069適|U+09002适|
+U+090C1郁|U+090C1郁|U+09B31鬱|
+U+0915D酝|U+0919E醞|U+09196醖|
+U+09170酰|U+09170酰|U+091AF醯|
+U+09178酸|U+09178酸|U+075E0痠|
+U+091C7采|U+091C7采|U+063A1採|U+057F0埰|
+U+091CC里|U+091CC里|U+088E1裡|U+088CF裏|
+U+093AD鎭|U+093AE鎮|
+U+0949F钟|U+0937E鍾|U+09418鐘|
+U+094A9钩|U+09264鉤|U+0920E鈎|
+U+094B5钵|U+07F3D缽|U+09262鉢|
+U+094F2铲|U+093DF鏟|U+05277剷|
+U+09508锈|U+092B9銹|U+093FD鏽|
+U+09510锐|U+092B3銳|U+092ED鋭|
+U+09528锨|U+06774杴|U+09341鍁|
+U+0954C镌|U+0942B鐫|U+093B8鎸|
+U+09562镢|U+09481钁|U+0941D鐝|
+U+09605阅|U+095B1閱|U+095B2閲|
+U+096C7雇|U+096C7雇|U+050F1僱|
+U+096D5雕|U+096D5雕|U+09D70鵰|
+U+09709霉|U+09709霉|U+09EF4黴|
+U+09762面|U+09762面|U+09EB5麵|U+09EAA麪|U+09EAB麫|
+U+097B2鞲|U+097DD韝|
+U+0987B须|U+09808須|U+09B1A鬚|
+U+09893颓|U+09839頹|U+0983D頽|
+U+0989C颜|U+0984F顏|U+09854顔|
+U+09965饥|U+098E2飢|U+09951饑|
+U+09980馀|U+09918餘|
+U+09986馆|U+09928館|U+08218舘|
+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+09E6E鹮|U+04D09䴉|
+U+09F44齄|U+09F47齇|
+U+0E82D|U+068E1棡|
+U+20BB6𠮶|U+055F0嗰|
+U+26216𦈖|U+04308䌈|
U+28C3E𨰾|U+093B7鎷|
U+28C3F𨰿|U+091F3釳|
U+28C40𨱀|U+2895B𨥛|
@@ -322,6 +343,7 @@ U+29F88𩾈|U+04C59䱙|
U+29F8A𩾊|U+04C6C䱬|
U+29F8B𩾋|U+04C70䱰|
U+29F8C𩾌|U+09C47鱇|
+U+29F8C𩾌|U+09C47鱇|
U+29F8E𩾎|U+29F47𩽇|
U+2A242𪉂|U+04CB0䲰|
U+2A243𪉃|U+09CFC鳼|
@@ -347,15 +369,4 @@ U+2A38A𪎊|U+09EA8麨|
U+2A38B𪎋|U+04D34䴴|
U+2A38C𪎌|U+09EB3麳|
U+2A68F𪚏|U+2A600𪘀|
-U+2A690𪚐|U+2A62F𪘯|
-U+0621A戚|U+0621A戚|U+0617C慼|U+093DA鏚|
-U+057FC埼|U+057FC埼|U+07895碕|
-U+065D7旗|U+065D7旗|U+065C2旂|
-U+06328挨|U+06328挨|U+06371捱|
-U+06EAF溯|U+06EAF溯|U+06CDD泝|
-U+09178酸|U+09178酸|U+075E0痠|
-U+07A57穗|U+07A57穗|U+07E50繐|
-U+084D1蓑|U+084D1蓑|U+07C11簑|
-U+26216𦈖|U+04308䌈|
-U+03CE0㳠|U+06FBE澾|
-U+0447D䑽|U+26A99𦪙| \ No newline at end of file
+U+2A690𪚐|U+2A62F𪘯| \ No newline at end of file
diff --git a/includes/zhtable/simpphrases.manual b/includes/zhtable/simpphrases.manual
index 6339ff83..a015a34b 100644
--- a/includes/zhtable/simpphrases.manual
+++ b/includes/zhtable/simpphrases.manual
@@ -111,6 +111,8 @@
乾龙
乾,健也
乾,天也
+乾健也
+乾天也
坤乾
天道为乾
尼乾陀
@@ -119,13 +121,20 @@
旋乾转坤
易·乾
《易乾
-《周易乾
+周易乾
易经·乾
易经乾
李乾德
萧乾
郭子乾
雍乾
+乾务
+乾沓和
+乾沓婆
+乾通
+乾忠
+乾淳
+李乾顺
不着痕迹
不着边际
与着
@@ -448,6 +457,8 @@
含著称
含著者
含著述
+听得着
+听不着
听着
听著书
听著作
@@ -824,16 +835,9 @@
扛著称
扛著者
扛著述
+找得着
找不着
-找不著书
-找不著作
-找不著名
-找不著录
-找不著称
-找不著者
-找不著述
抓着
-抓著书
抓著作
抓著名
抓著录
@@ -849,7 +853,6 @@
披著者
披著述
抬着
-抬著书
抬著作
抬著名
抬著录
@@ -857,7 +860,6 @@
抬著者
抬著述
抱着
-抱著书
抱著作
抱著名
抱著录
@@ -873,7 +875,6 @@
拉著者
拉著述
拎着
-拎著书
拎著作
拎著名
拎著录
@@ -881,7 +882,6 @@
拎著者
拎著述
拖着
-拖著书
拖著作
拖著名
拖著录
@@ -889,7 +889,6 @@
拖著者
拖著述
拼着
-拼著书
拼著作
拼著名
拼著录
@@ -897,7 +896,6 @@
拼著者
拼著述
拿着
-拿著书
拿著作
拿著名
拿著录
@@ -905,7 +903,6 @@
拿著者
拿著述
持着
-持著书
持著作
持著名
持著录
@@ -913,7 +910,6 @@
持著者
持著述
挑着
-挑著书
挑著作
挑著名
挑著录
@@ -921,7 +917,6 @@
挑著者
挑著述
挡着
-挡著书
挡著作
挡著名
挡著录
@@ -937,7 +932,6 @@
挣著者
挣著述
挥着
-挥著书
挥著作
挥著名
挥著录
@@ -945,7 +939,6 @@
挥著者
挥著述
挨着
-挨著书
挨著作
挨著名
挨著录
@@ -953,7 +946,6 @@
挨著者
挨著述
捆着
-捆著书
捆著作
捆著名
捆著录
@@ -969,7 +961,6 @@
据著者
据著述
掖着
-掖著书
掖著作
掖著名
掖著录
@@ -977,7 +968,6 @@
掖著者
掖著述
接着
-接著书
接著作
接著名
接著录
@@ -993,7 +983,6 @@
揉著者
揉著述
提着
-提著书
提著作
提著名
提著录
@@ -1001,7 +990,6 @@
提著者
提著述
搂着
-搂著书
搂著作
搂著名
搂著录
@@ -1009,7 +997,6 @@
搂著者
搂著述
摆着
-摆著书
摆著作
摆著名
摆著录
@@ -1025,7 +1012,6 @@
撼著者
撼著述
敞着
-敞著书
敞著作
敞著名
敞著录
@@ -1033,7 +1019,6 @@
敞著者
敞著述
数着
-数著书
数著作
数著名
数著录
@@ -1073,7 +1058,6 @@
映著者
映著述
晃着
-晃著书
晃著作
晃著名
晃著录
@@ -1097,7 +1081,6 @@
有著者
有著述
望着
-望著书
望著作
望著名
望著录
@@ -1105,7 +1088,6 @@
望著者
望著述
朝着
-朝著书
朝著作
朝著名
朝著录
@@ -1145,7 +1127,6 @@
来著者
来著述
枕着
-枕著书
枕著作
枕著名
枕著录
@@ -1161,7 +1142,6 @@
梦著者
梦著述
梳着
-梳著书
梳著作
梳著名
梳著录
@@ -1302,6 +1282,7 @@
牵著称
牵著者
牵著述
+犯得着
犯不着
独着
独著书
@@ -1327,14 +1308,8 @@
甜著称
甜著者
甜著述
+用得着
用不着
-用不着书
-用不著作
-用不著名
-用不著录
-用不著称
-用不著者
-用不著述
用着
用著书
用著作
@@ -1391,6 +1366,8 @@
盾著称
盾著者
盾著述
+看得着
+看不着
看着
看着书
看著作
@@ -1399,6 +1376,14 @@
看著称
看著者
看著述
+瞧着
+瞧着书
+瞧著作
+瞧著名
+瞧著录
+瞧著称
+瞧著者
+瞧著述
着业
着丝
着么
@@ -1501,14 +1486,8 @@
着鞭
着题
着魔
+睡得着
睡不着
-睡不著书
-睡不著作
-睡不著名
-睡不著录
-睡不著称
-睡不著者
-睡不著述
睡着
睡著书
睡著作
@@ -2150,6 +2129,37 @@
护著称
护著者
护著述
+保护着
+爱护着
+庇护着
+传着
+传著书
+传著作
+传著名
+传著录
+传著称
+传著者
+传著述
+标志着
+流露着
+靠着
+靠著作
+靠著名
+靠著录
+靠著称
+靠著者
+靠著述
+玩着
+迫着
+吃得着
+吃不着
+吃着
+闻得着
+闻不着
+闻着
+嗅得着
+嗅不着
+嗅着
於乎
於戏
魏徵
@@ -2162,13 +2172,19 @@
樊於期
於菟
於潜县
-馀年
石碁镇
因著《
因著〈
李泽钜
於祥玉
於崇文
+於世成
+於乙宇同
+於宇同
+朴於宇同
+於哲
+於除鞬
+於志贺
覆蓋
五箇山
麽麽
@@ -2197,4 +2213,12 @@
幺谦
麴义
麴英
-麯崇裕 \ No newline at end of file
+麯崇裕
+阿部正瞭
+醯酱
+醯鸡
+醯醋
+醯醢
+醯壶
+苧烯
+近角聪信
diff --git a/includes/zhtable/simpphrases_exclude.manual b/includes/zhtable/simpphrases_exclude.manual
index a8f61e09..4606041f 100644
--- a/includes/zhtable/simpphrases_exclude.manual
+++ b/includes/zhtable/simpphrases_exclude.manual
@@ -15,4 +15,6 @@
-樑 \ No newline at end of file
+樑
+摺叠
+餗 \ No newline at end of file
diff --git a/includes/zhtable/toCN.manual b/includes/zhtable/toCN.manual
index 5bfcc00c..e3c12d0b 100644
--- a/includes/zhtable/toCN.manual
+++ b/includes/zhtable/toCN.manual
@@ -24,6 +24,10 @@
字型檔 字库
欄位 字段
字元 字符
+字元济 字元济
+字元濟 字元济
+字元会 字元会
+字元會 字元会
存檔 存盘
定址 寻址
章節附註 尾注
@@ -82,13 +86,14 @@
掃瞄器 扫瞄仪
寬頻 宽带
資料庫 数据库
-乳酪 奶酪
鉅賈 巨商
萬曆 万历
永曆 永历
辭彙 词汇
母音 元音
自由球 任意球
+自由球员 自由球员
+自由球員 自由球员
頭槌 头球
進球 入球
顆進球 粒入球
@@ -223,6 +228,7 @@
夜学 夜校
华乐 民乐
中樂 民乐
+华乐街 华乐街
屋价 房价
計程車 出租车
公車 公共汽车
@@ -249,8 +255,10 @@
拿破崙 拿破仑
布殊 布什
布希 布什
+布希亞 布希亚
+布希亚 布希亚
柯林頓 克林顿
-海珊 萨达姆
+海珊 侯赛因
梵谷 凡高
大衛碧咸 大卫·贝克汉姆
米高奧雲 迈克尔·欧文
diff --git a/includes/zhtable/toHK.manual b/includes/zhtable/toHK.manual
index a39d6409..10a3dfcb 100644
--- a/includes/zhtable/toHK.manual
+++ b/includes/zhtable/toHK.manual
@@ -2,6 +2,8 @@
“ 「
‘ 『
’ 』
+鉤 鈎
+衛 衞
凶殺 兇殺
凶殘 兇殘
緝凶 緝兇
@@ -134,6 +136,8 @@
拿破崙 拿破侖
布什 布殊
布希 布殊
+布希亞 布希亞
+布希亚 布希亞
柯林頓 克林頓
萨达姆 薩達姆
海珊 侯賽因
@@ -197,6 +201,7 @@
獨著 獨着
對著 對着
盾著 盾着
+犯得著 犯得着
犯不著 犯不着
福著 福着
趕著 趕着
@@ -232,6 +237,8 @@
借著 借着
據著 據着
開著 開着
+看得著 看得着
+看不著 看不着
看著 看着
康著 康着
扛著 扛着
@@ -300,6 +307,7 @@
梳著 梳着
豎著 豎着
數著 數着
+睡得著 睡得着
睡不著 睡不着
睡著 睡着
順著 順着
@@ -311,7 +319,8 @@
甜著 甜着
挑著 挑着
跳著 跳着
-聽著 聽着
+聽得著 聽得着
+聽不著 聽不着
聽著 聽着
偷著 偷着
拖著 拖着
@@ -342,6 +351,7 @@
印著 印着
應著 應着
映著 映着
+用得著 用得着
用不著 用不着
用著 用着
悠著 悠着
@@ -360,6 +370,7 @@
戰著 戰着
蘸著 蘸着
仗著 仗着
+找得著 找得着
找不著 找不着
照著 照着
罩著 罩着
@@ -436,7 +447,6 @@
挨著述 挨著述
挨著稱 挨著稱
挨著錄 挨著錄
-挨著書 挨著書
愛著作 愛著作
愛著者 愛著者
愛著名 愛著名
@@ -464,7 +474,6 @@
擺著述 擺著述
擺著稱 擺著稱
擺著錄 擺著錄
-擺著書 擺著書
伴著作 伴著作
伴著者 伴著者
伴著名 伴著名
@@ -499,7 +508,6 @@
抱著述 抱著述
抱著稱 抱著稱
抱著錄 抱著錄
-抱著書 抱著書
背著作 背著作
背著者 背著者
背著名 背著名
@@ -583,7 +591,6 @@
敞著述 敞著述
敞著稱 敞著稱
敞著錄 敞著錄
-敞著書 敞著書
唱著作 唱著作
唱著者 唱著者
唱著名 唱著名
@@ -597,7 +604,6 @@
朝著述 朝著述
朝著稱 朝著稱
朝著錄 朝著錄
-朝著書 朝著書
沉著作 沉著作
沉著者 沉著者
沉著名 沉著名
@@ -618,7 +624,6 @@
持著述 持著述
持著稱 持著稱
持著錄 持著錄
-持著書 持著書
斥著作 斥著作
斥著者 斥著者
斥著名 斥著名
@@ -695,7 +700,6 @@
擋著述 擋著述
擋著稱 擋著稱
擋著錄 擋著錄
-擋著書 擋著書
得著作 得著作
得著者 得著者
得著名 得著名
@@ -905,14 +909,12 @@
晃著述 晃著述
晃著稱 晃著稱
晃著錄 晃著錄
-晃著書 晃著書
揮著作 揮著作
揮著者 揮著者
揮著名 揮著名
揮著述 揮著述
揮著稱 揮著稱
揮著錄 揮著錄
-揮著書 揮著書
活著作 活著作
活著者 活著者
活著名 活著名
@@ -996,7 +998,6 @@
接著述 接著述
接著稱 接著稱
接著錄 接著錄
-接著書 接著書
借著作 借著作
借著者 借著者
借著名 借著名
@@ -1094,7 +1095,6 @@
捆著述 捆著述
捆著稱 捆著稱
捆著錄 捆著錄
-捆著書 捆著書
困著作 困著作
困著者 困著者
困著名 困著名
@@ -1178,7 +1178,6 @@
拎著述 拎著述
拎著稱 拎著稱
拎著錄 拎著錄
-拎著書 拎著書
領著作 領著作
領著者 領著者
領著名 領著名
@@ -1206,7 +1205,6 @@
摟著述 摟著述
摟著稱 摟著稱
摟著錄 摟著錄
-摟著書 摟著書
陋著作 陋著作
陋著者 陋著者
陋著名 陋著名
@@ -1283,7 +1281,6 @@
拿著述 拿著述
拿著稱 拿著稱
拿著錄 拿著錄
-拿著書 拿著書
逆著作 逆著作
逆著者 逆著者
逆著名 逆著名
@@ -1360,7 +1357,6 @@
拼著述 拼著述
拼著稱 拼著稱
拼著錄 拼著錄
-拼著書 拼著書
鋪著作 鋪著作
鋪著者 鋪著者
鋪著名 鋪著名
@@ -1486,7 +1482,6 @@
梳著述 梳著述
梳著稱 梳著稱
梳著錄 梳著錄
-梳著書 梳著書
豎著作 豎著作
豎著者 豎著者
豎著名 豎著名
@@ -1500,14 +1495,6 @@
數著述 數著述
數著稱 數著稱
數著錄 數著錄
-數著書 數著書
-睡不著作 睡不著作
-睡不著者 睡不著者
-睡不著名 睡不著名
-睡不著述 睡不著述
-睡不著稱 睡不著稱
-睡不著錄 睡不著錄
-睡不著書 睡不著書
睡著作 睡著作
睡著者 睡著者
睡著名 睡著名
@@ -1535,14 +1522,12 @@
踏著述 踏著述
踏著稱 踏著稱
踏著錄 踏著錄
-踏著書 踏著書
抬著作 抬著作
抬著者 抬著者
抬著名 抬著名
抬著述 抬著述
抬著稱 抬著稱
抬著錄 抬著錄
-抬著書 抬著書
躺著作 躺著作
躺著者 躺著者
躺著名 躺著名
@@ -1556,7 +1541,6 @@
提著述 提著述
提著稱 提著稱
提著錄 提著錄
-提著書 提著書
甜著作 甜著作
甜著者 甜著者
甜著名 甜著名
@@ -1570,7 +1554,6 @@
挑著述 挑著述
挑著稱 挑著稱
挑著錄 挑著錄
-挑著書 挑著書
跳著作 跳著作
跳著者 跳著者
跳著名 跳著名
@@ -1585,13 +1568,6 @@
聽著稱 聽著稱
聽著錄 聽著錄
聽著書 聽著書
-聽著作 聽著作
-聽著者 聽著者
-聽著名 聽著名
-聽著述 聽著述
-聽著稱 聽著稱
-聽著錄 聽著錄
-聽著書 聽著書
偷著作 偷著作
偷著者 偷著者
偷著名 偷著名
@@ -1605,7 +1581,6 @@
拖著述 拖著述
拖著稱 拖著稱
拖著錄 拖著錄
-拖著書 拖著書
望著作 望著作
望著者 望著者
望著名 望著名
@@ -1738,7 +1713,6 @@
掖著述 掖著述
掖著稱 掖著稱
掖著錄 掖著錄
-掖著書 掖著書
衣著作 衣著作
衣著者 衣著者
衣著名 衣著名
@@ -1795,13 +1769,6 @@
映著稱 映著稱
映著錄 映著錄
映著書 映著書
-用不著作 用不著作
-用不著者 用不著者
-用不著名 用不著名
-用不著述 用不著述
-用不著稱 用不著稱
-用不著錄 用不著錄
-用不著書 用不著書
用著作 用著作
用著者 用著者
用著名 用著名
@@ -1921,13 +1888,6 @@
仗著稱 仗著稱
仗著錄 仗著錄
仗著書 仗著書
-找不著作 找不著作
-找不著者 找不著者
-找不著名 找不著名
-找不著述 找不著述
-找不著稱 找不著稱
-找不著錄 找不著錄
-找不著書 找不著書
照著作 照著作
照著者 照著者
照著名 照著名
@@ -1955,7 +1915,6 @@
枕著述 枕著述
枕著稱 枕著稱
枕著錄 枕著錄
-枕著書 枕著書
爭著作 爭著作
爭著者 爭著者
爭著名 爭著名
@@ -2004,7 +1963,6 @@
抓著述 抓著述
抓著稱 抓著稱
抓著錄 抓著錄
-抓著書 抓著書
轉著作 轉著作
轉著者 轉著者
轉著名 轉著名
@@ -2138,8 +2096,146 @@
殺著稱 殺著稱
殺著錄 殺著錄
殺著書 殺著書
+標誌著 標誌着
+幹著 幹着
+干着 幹着
+干着急 干着急
+流露著 流露着
+靠著 靠着
+靠著作 靠著作
+靠著名 靠著名
+靠著錄 靠著錄
+靠著录 靠著錄
+靠著稱 靠著稱
+靠著称 靠著稱
+靠著者 靠著者
+靠著述 靠著述
新著龍虎門 新著龍虎門
+迫著 迫着
+心繫著 心繫着
+藉著 藉着
+吃得著 吃得着
+吃不著 吃不着
+吃著 吃着
+聞得著 闻得着
+聞不著 闻不着
+聞著 闻着
+嗅得著 嗅得着
+嗅不著 嗅不着
+嗅著 嗅着
榴莲 榴槤
榴蓮 榴槤
发布 發佈
-發布 發佈 \ No newline at end of file
+發布 發佈
+掛鉤 掛鈎
+鉤心鬥角 鈎心鬥角
+咤 咤
+叱吒 叱咤
+叱咤 叱咤
+醯 酰
+醯醬 醯醬
+醯雞 醯雞
+醯酱 醯醬
+醯鸡 醯雞
+醯醋 醯醋
+醯醢 醯醢
+醯壶 醯壺
+醯壺 醯壺
+菸 煙
+雪裡紅 雪裏紅
+雪裡蕻 雪裏蕻
+雪里蕻 雪裏蕻
+雪里红 雪裏紅
+森林裡 森林裏
+森林里 森林裏
+日子裡 日子裏
+日子里 日子裏
+故事裡 故事裏
+故事里 故事裏
+領域裡 領域裏
+领域里 領域裏
+時間裡 時間裏
+时间里 時間裏
+深淵裡 深淵裏
+深渊里 深渊裏
+醫院裡 醫院裏
+医院里 医院裏
+春假裡 春假裏
+春假里 春假裏
+暑假裡 暑假裏
+暑假里 暑假裏
+秋假裡 秋假裏
+秋假里 秋假裏
+寒假裡 寒假裏
+寒假里 寒假裏
+春天裡 春天裏
+春天里 春天裏
+夏天裡 夏天裏
+夏天里 夏天裏
+秋天裡 秋天裏
+秋天里 秋天裏
+冬天裡 冬天裏
+冬天里 冬天裏
+春日裡 春日裏
+夏日裡 夏日裏
+秋日裡 秋日裏
+冬日裡 冬日裏
+春日里 春日裏
+夏日里 夏日裏
+秋日里 秋日裏
+冬日里 冬日裏
+嘴裡 嘴裏
+嘴里 嘴裏
+心裡 心裏
+心里 心裏
+皮裡陽秋 皮裏陽秋
+皮里阳秋 皮裏陽秋
+肚裡 肚裏
+肚里 肚裏
+苦裡 苦裏
+苦里 苦裏
+裡勾外連 裏勾外連
+里勾外连 裏勾外連
+裡面 裏面
+里面 裏面
+這裡 這裏
+這里 這裏
+點裡 點裏
+点里 點裏
+中文裡 中文裏
+中文里 中文裏
+山洞里 山洞裏
+山洞裡 山洞裏
+近角聪信 近角聰信
+近角聰信 近角聰信
+世界里 世界裏
+世界裡 世界裏
+眼睛里 眼睛裏
+眼睛裡 眼睛裏
+百科裡 百科裏
+百科里 百科裏
+歷史裡 歷史裏
+历史里 歷史裏
+戲裡 戲裏
+戏里 戲裏
+作品裡 作品裏
+作品里 作品裏
+專輯裡 專輯裏
+专辑里 專輯裏
+年代裡 年代裏
+年代里 年代裏
+棺材裡 棺材裏
+棺材里 棺材裏
+學裡 學裏
+学里 學裏
+獄裡 獄裏
+狱里 獄裏
+館裡 館裏
+馆里 館裏
+系列裡 系列裏
+系列里 系列裏
+村子裡 村子裏
+村子里 村子裏
+分布 分佈
+分布于 分佈於
+分布於 分佈於
diff --git a/includes/zhtable/toSimp.manual b/includes/zhtable/toSimp.manual
index c02ed00e..f424ee73 100644
--- a/includes/zhtable/toSimp.manual
+++ b/includes/zhtable/toSimp.manual
@@ -44,10 +44,12 @@
天道为乾 天道为乾
易经·乾 易经·乾
易经乾 易经乾
+乾务 乾务
柳诒徵 柳诒徵
於夫罗 於夫罗
於梨华 於梨华
於潜县 於潜县
+於志贺 於志贺
憑藉 凭借
藉端 借端
藉故 借故
@@ -58,6 +60,8 @@
藉機 借机
藉此 借此
藉由 借由
+藉著 借着
+藉着 借着
沈積 沉积
沈船 沉船
沈默 沉默
@@ -116,4 +120,23 @@
么二三 幺二三
么篇 幺篇
么謙 幺谦
-麴义 麴义 \ No newline at end of file
+麴义 麴义
+乾乾淨淨 干干净净
+乾乾脆脆 干干脆脆
+肉乾乾 肉干干
+魚乾乾 鱼干干
+於于同 於于同
+於乙于同 於乙于同
+閻懷禮 闫怀礼
+醯酱 醯酱
+醯鸡 醯鸡
+醯壶 醯壶
+苧烯 苧烯
+李乾顺 李乾顺
+幹著 干着
+氾濫 泛滥
+显著 显著
+顯著 显著
+標誌著 标志着
+近角聪信 近角聪信
+修鍊 修炼
diff --git a/includes/zhtable/toTW.manual b/includes/zhtable/toTW.manual
index 1cc527fa..2ce16f3f 100644
--- a/includes/zhtable/toTW.manual
+++ b/includes/zhtable/toTW.manual
@@ -3,6 +3,9 @@
‘ 『
’ 』
着 著
+鈎 鉤
+钩 鉤
+衞 衛
元凶 元凶
元兇 元凶
凶器 凶器
@@ -124,6 +127,8 @@
新紀元 新紀元
宋元 宋元
任意球 自由球
+任意球员 任意球員
+任意球員 任意球員
头球 頭槌
入球 進球
粒入球 顆進球
@@ -317,7 +322,6 @@
出租车 計程車
台球 撞球
桌球 撞球
-雪糕 冰淇淋
卫生 衛生
衞生 衛生
平治之亂 平治之亂
@@ -354,4 +358,55 @@
戏彩娱亲 戲綵娛親
剪彩 剪綵
榴莲 榴槤
-榴蓮 榴槤 \ No newline at end of file
+榴蓮 榴槤
+掛鈎 掛鉤
+挂钩 掛鉤
+鈎心鬥角 鉤心鬥角
+钩心斗角 鉤心鬥角
+酰 醯
+雪裏紅 雪裡紅
+雪裏蕻 雪裡蕻
+森林裏 森林裡
+日子裏 日子裡
+故事裏 故事裡
+領域裏 領域裡
+時間裏 時間裡
+深淵裏 深淵裡
+醫院裏 醫院裡
+春假裏 春假裡
+暑假裏 暑假裡
+秋假裏 秋假裡
+寒假裏 寒假裡
+春天裏 春天裡
+夏天裏 夏天裡
+秋天裏 秋天裡
+冬天裏 冬天裡
+春日裏 春日裡
+夏日裏 夏日裡
+秋日裏 秋日裡
+冬日裏 冬日裡
+百科裏 百科裡
+歷史裏 歷史裡
+戲裏 戲裡
+作品裏 作品裡
+專輯裏 專輯裡
+年代裏 年代裡
+棺材裏 棺材裡
+嘴裏 嘴裡
+心裏 心裡
+皮裏陽秋 皮裡陽秋
+肚裏 肚裡
+苦裏 苦裡
+裏勾外連 裡勾外連
+裏面 裡面
+這裏 這裡
+點裏 點裡
+中文裏 中文裡
+山洞裏 山洞裡
+世界裏 世界裡
+眼睛裏 眼睛裡
+學裏 學裡
+獄裏 獄裡
+館裏 館裡
+系列裏 系列裡
+村子裏 村子裡
diff --git a/includes/zhtable/toTrad.manual b/includes/zhtable/toTrad.manual
index b392adc5..b3459054 100644
--- a/includes/zhtable/toTrad.manual
+++ b/includes/zhtable/toTrad.manual
@@ -64,4 +64,79 @@
不好干預 不好干預
不干預 不干預
不干擾 不干擾
-不干牠 不干牠 \ No newline at end of file
+不干牠 不干牠
+矽谷 矽谷
+范文瀾 范文瀾
+發表 發表
+機械系 機械系
+頂多 頂多
+馬占山 馬占山
+叱咤樂壇 叱咤樂壇
+闫怀礼 閆懷禮
+变髒 變髒
+薴烯 薴烯
+后豐 后豐
+于謙 于謙
+詩云 詩云
+鄭凱云 鄭凱云
+云為 云為
+古書云 古書云
+古語云 古語云
+經有云 經有云
+語有云 語有云
+显著标志 顯著標志
+占領 佔領
+采納 採納
+風采 風采
+于樂 于樂
+于軍 于軍
+于堅 于堅
+于帥 于帥
+于濤 于濤
+于贈 于贈
+于會泳 于會泳
+于偉國 于偉國
+于光遠 于光遠
+于鳳至 于鳳至
+于台煙 于台煙
+于國楨 于國楨
+于大寶 于大寶
+于學忠 于學忠
+于小偉 于小偉
+于山國 于山國
+于幼軍 于幼軍
+于廣洲 于廣洲
+于從濂 于從濂
+于志寧 于志寧
+于成龍 于成龍
+于明濤 于明濤
+于根偉 于根偉
+于樹潔 于樹潔
+于正昇 于正昇
+于漢超 于漢超
+于洪區 于洪區
+于湘蘭 于湘蘭
+于蔭霖 于蔭霖
+于遠偉 于遠偉
+于都縣 于都縣
+于震寰 于震寰
+于震環 于震環
+于非闇 于非闇
+于風政 于風政
+于鳳桐 于鳳桐
+于默奧 于默奧
+于爾岑 于爾岑
+于默奧 于默奧
+于貝爾 于貝爾
+于爾根 于爾根
+于雙戈 于雙戈
+于澤爾 于澤爾
+于斯達爾 于斯達爾
+于爾里克 于爾里克
+于奇庫杜克 于奇庫杜克
+于韋斯屈萊 于韋斯屈萊
+于克-蘭多縣 于克-蘭多縣
+于斯納爾斯貝里 于斯納爾斯貝里
+涂澤民 涂澤民
+涂長望 涂長望
+台历 枱曆 \ No newline at end of file
diff --git a/includes/zhtable/trad2simp.manual b/includes/zhtable/trad2simp.manual
index d5bcfa0e..4aed7e1d 100644
--- a/includes/zhtable/trad2simp.manual
+++ b/includes/zhtable/trad2simp.manual
@@ -1,139 +1,143 @@
-U+056a5嚥|U+054bd咽|
-U+0585a塚|U+051a2冢|
-U+05dbd嶽|U+05cb3岳|
-U+04e99亙|U+04e98亘|
-U+081e5臥|U+05367卧|
-U+04f48佈|U+05e03布|
-U+06dd2淒|U+051c4凄|
-U+06de8淨|U+051c0净|
-U+05147兇|U+051f6凶|
-U+04f48佈|U+05e03布|
-U+06c59汙|U+06c61污|
-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昆|
-U+06372捲|U+05377卷|
-U+07D2E紮|U+0624E扎|
-U+07C64籤|U+07B7E签|
-U+05925夥|U+04F19伙|
-U+05F46彆|U+0522B别|
-U+09D70鵰|U+096D5雕|U+05F6B彫|
-U+0617E慾|U+06B32欲|
-U+07A1C稜|U+068F1棱|
-U+06A11樑|U+06881梁|
+U+03473㑳|U+03447㑇|
+U+04308䌈|U+26216𦈖|
+U+04A8F䪏|U+293FC𩏼|
+U+04A97䪗|U+29400𩐀|
+U+04A98䪘|U+293FF𩏿|
+U+04AF4䫴|U+29597𩖗|
+U+04B18䬘|U+2966E𩙮|
+U+04B1D䬝|U+2966F𩙯|
+U+04B40䭀|U+29807𩠇|
+U+04B43䭃|U+29808𩠈|
+U+04B7F䭿|U+299ED𩧭|
+U+04B9D䮝|U+299F0𩧰|
+U+04B9E䮞|U+29A01𩨁|
+U+04BA0䮠|U+299FF𩧿|
+U+04BB3䮳|U+29A0F𩨏|
+U+04BBE䮾|U+299EA𩧪|
+U+04C59䱙|U+29F88𩾈|
+U+04C6C䱬|U+29F8A𩾊|
+U+04C70䱰|U+29F8B𩾋|
+U+04C77䱷|U+04CA3䲣|
+U+04CB0䲰|U+2A242𪉂|
+U+04D2C䴬|U+2A388𪎈|
+U+04D34䴴|U+2A38B𪎋|
+U+04E99亙|U+04E98亘|
+U+04F48佈|U+05E03布|
+U+04F48佈|U+05E03布|
U+04F54佔|U+05360占|
U+05016倖|U+05E78幸|
U+050A2傢|U+05BB6家|
U+050F1僱|U+096C7雇|
+U+05138儸|U+03469㑩|U+07F57罗|
+U+05147兇|U+051F6凶|
+U+05277剷|U+094F2铲|
+U+052F3勳|U+052CB勋|
+U+0537D卽|U+05373即|
U+053A4厤|U+05386历|
+U+055AB喫|U+05403吃|
+U+055F0嗰|U+20BB6𠮶|
U+05641噁|U+06076恶|
+U+05690嚐|U+05C1D尝|
+U+056A5嚥|U+054BD咽|
+U+056AE嚮|U+05411向|
U+056CC囌|U+082CF苏|
+U+0585A塚|U+051A2冢|
+U+058B0墰|U+0575B坛|
+U+058DC壜|U+0575B坛|
+U+05925夥|U+04F19伙|
+U+05BC0寀|U+091C7采|
+U+05D11崑|U+06606昆|
U+05D19崙|U+04ED1仑|
+U+05D57嵗|U+05C81岁|
+U+05DBD嶽|U+05CB3岳|
+U+05DD6巖|U+05CA9岩|
+U+05DF9巹|U+0537A卺|
U+05F14弔|U+0540A吊|
+U+05F46彆|U+0522B别|
+U+0617C慼|U+0621A戚|
+U+0617E慾|U+06B32欲|
U+061DE懞|U+08499蒙|
U+062DA拚|U+062FC拼|
+U+06331挱|U+06332挲|
+U+06371捱|U+06328挨|
+U+06372捲|U+05377卷|
U+0647A摺|U+06298折|
+U+065C2旂|U+065D7旗|
+U+065E3旣|U+065E2既|
U+06607昇|U+05347升|
U+0672E朮|U+0672F术|
+U+068CA棊|U+068CB棋|
+U+068E1棡|U+0E82D|
U+069A6榦|U+05E72干|
+U+069D3槓|U+06760杠|
+U+06A11樑|U+06881梁|
+U+06B05欅|U+06989榉|
+U+06B4E歎|U+053F9叹|
+U+06BAD殭|U+050F5僵|
+U+06C59汙|U+06C61污|
+U+06CDD泝|U+06EAF溯|
+U+06D29洩|U+06CC4泄|
+U+06DD2淒|U+051C4凄|
+U+06DE8淨|U+051C0净|
+U+06DE9淩|U+051CC凌|
U+06E67湧|U+06D8C涌|
U+06ED9滙|U+06C47汇|
U+06F90澐|U+06C84沄|
-U+06FDB濛|U+08499蒙|
+U+06FBE澾|U+03CE0㳠|
+U+06FDB濛|U+06FDB濛|U+08499蒙|
U+07030瀰|U+05F25弥|
+U+071EC燬|U+06BC1毁|
+U+07343獃|U+05446呆|
+U+07515甕|U+074EE瓮|
U+07526甦|U+082CF苏|
-U+07575畵|U+0753B画|U+05212划|
U+0756B畫|U+0753B画|U+05212划|
+U+07575畵|U+0753B画|U+05212划|
+U+075E0痠|U+09178酸|
+U+07652癒|U+06108愈|
+U+07661癡|U+075F4痴|
U+076C3盃|U+0676F杯|
U+077AD瞭|U+04E86了|
U+077C7矇|U+08499蒙|
U+07843硃|U+06731朱|
+U+07895碕|U+057FC埼|
+U+07958祘|U+07B97算|
+U+07A1C稜|U+068F1棱|
+U+07B87箇|U+04E2A个|
+U+07C11簑|U+084D1蓑|
+U+07C64籤|U+07B7E签|
U+07C72籲|U+05401吁|
U+07CF0糰|U+056E2团|
+U+07D2E紮|U+0624E扎|
+U+07DB5綵|U+05F69彩|U+0433D䌽|
U+07E34縴|U+07EA4纤|
+U+07E50繐|U+07A57穗|
U+07E94纔|U+0624D才|
+U+07F4E罎|U+0575B坛|
+U+07FA8羨|U+07FA1羡|
+U+08123脣|U+05507唇|
+U+081E5臥|U+05367卧|
+U+08218舘|U+09986馆|
+U+083F4菴|U+05EB5庵|
+U+08457著|U+08457著|U+07740着|
+U+08518蔘|U+053C2参|
U+08591薑|U+059DC姜|
+U+085C9藉|U+085C9藉|U+0501F借|
+U+0880D蠍|U+0874E蝎|
U+0884A衊|U+08511蔑|
+U+08946襆|U+05E5E幞|
+U+08986覆|U+08986覆|U+0590D复|
U+08A17託|U+06258托|U+08BAC讬|
+U+08AEE諮|U+054A8咨|U+08C18谘|
+U+08B6D譭|U+06BC1毁|
U+08B8E讎|U+04EC7仇|
U+08B9A讚|U+08D5E赞|
-U+08FF4迴|U+056DE回|
-U+0955F镟|U+065CB旋|
-U+095A4閤|U+05408合|
-U+0965E陞|U+05347升|
-U+097A6鞦|U+079CB秋|U+097A7鞧|
-U+097C6韆|U+05343千|
-U+09935餵|U+05582喂|
-U+09B28鬨|U+054C4哄|
-U+09EAA麪|U+09762面|
-U+09EAB麫|U+09762面|
-U+09EAF麯|U+066F2曲|
-U+09EF4黴|U+09709霉|
-U+09F15鼕|U+051AC冬|
-U+09F63齣|U+051FA出|
-U+068E1棡|U+0E82D|
U+08C54豔|U+08273艳|
-U+06B4E歎|U+053F9叹|
-U+0938C鎌|U+09570镰|
-U+07515甕|U+074EE瓮|
-U+07652癒|U+06108愈|
-U+069D3槓|U+06760杠|
-U+06D29洩|U+06CC4泄|
-U+09451鑑|U+09274鉴|
-U+08AEE諮|U+054A8咨|U+08C18谘|
-U+052F3勳|U+052CB勋|
-U+06BAD殭|U+050F5僵|
-U+09C47鱇|U+29F8C𩾌|
-U+03473㑳|U+03447㑇|
-U+09E7C鹼|U+078B1碱|U+07877硷|
-U+0962A阪|U+0962A阪|U+05742坂|
-U+0934A鍊|U+070BC炼|U+094FE链|
-U+08986覆|U+08986覆|U+0590D复|
-U+085C9藉|U+085C9藉|U+0501F借|
-U+05138儸|U+03469㑩|U+07F57罗|
-U+06FDB濛|U+06FDB濛|U+08499蒙|
-U+07B87箇|U+04E2A个|
-U+05277剷|U+094F2铲|
-U+05690嚐|U+05C1D尝|
-U+055AB喫|U+05403吃|
-U+07661癡|U+075F4痴|
-U+083F4菴|U+05EB5庵|
-U+07DB5綵|U+05F69彩|U+0433D䌽|
-U+08123脣|U+05507唇|
-U+055F0嗰|U+20BB6𠮶|
-U+04A8F䪏|U+293FC𩏼|
-U+04A97䪗|U+29400𩐀|
-U+04A98䪘|U+293FF𩏿|
-U+04AF4䫴|U+29597𩖗|
-U+04B18䬘|U+2966E𩙮|
-U+04B1D䬝|U+2966F𩙯|
-U+04B40䭀|U+29807𩠇|
-U+04B43䭃|U+29808𩠈|
-U+04B7F䭿|U+299ED𩧭|
-U+04B9D䮝|U+299F0𩧰|
-U+04B9E䮞|U+29A01𩨁|
-U+04BA0䮠|U+299FF𩧿|
-U+04BB3䮳|U+29A0F𩨏|
-U+04BBE䮾|U+299EA𩧪|
-U+04C59䱙|U+29F88𩾈|
-U+04C6C䱬|U+29F8A𩾊|
-U+04C70䱰|U+29F8B𩾋|
-U+04C77䱷|U+04CA3䲣|
-U+04CB0䲰|U+2A242𪉂|
-U+04D2C䴬|U+2A388𪎈|
-U+04D34䴴|U+2A38B𪎋|
+U+08FF4迴|U+056DE回|
+U+09031週|U+05468周|
+U+0904A遊|U+06E38游|
+U+09061遡|U+06EAF溯|
+U+091A3醣|U+07CD6糖|
+U+091AF醯|U+09170酰|
U+091F3釳|U+28C3F𨰿|
U+091FE釾|U+0497A䥺|
U+0920B鈋|U+28C42𨱂|
@@ -145,23 +149,40 @@ U+092B6銶|U+28C47𨱇|
U+092C9鋉|U+28C48𨱈|
U+09302錂|U+28C4B𨱋|
U+09344鍄|U+28C49𨱉|
+U+0934A鍊|U+070BC炼|U+094FE链|
U+0936E鍮|U+28C4E𨱎|
+U+0938C鎌|U+09570镰|
U+0939D鎝|U+28C4F𨱏|
+U+093AD鎭|U+093AE镇|
U+093AF鎯|U+28C4D𨱍|
U+093B7鎷|U+28C3E𨰾|
U+093C6鏆|U+28C4C𨱌|
U+093C9鏉|U+28C52𨱒|
+U+093DA鏚|U+0621A戚|
U+093FA鏺|U+0497D䥽|
U+0940E鐎|U+28C53𨱓|
U+0940F鐏|U+28C54𨱔|
U+09425鐥|U+04985䦅|
U+0942F鐯|U+04983䦃|
+U+09451鑑|U+09274鉴|
+U+0955F镟|U+065CB旋|
U+0958D閍|U+28E02𨸂|
U+09590閐|U+28E03𨸃|
+U+09592閒|U+095F2闲|
+U+095A4閤|U+05408合|
+U+095E2闢|U+08F9F辟|
+U+0962A阪|U+0962A阪|U+05742坂|
+U+0965E陞|U+05347升|
+U+097A6鞦|U+079CB秋|U+097A7鞧|
+U+097C6韆|U+05343千|
+U+097DD韝|U+097B2鞲|
U+09843顃|U+29596𩖖|
U+098B0颰|U+29665𩙥|
U+098B7颷|U+2966A𩙪|
U+098BE颾|U+2966B𩙫|
+U+098F1飱|U+098E7飧|
+U+09918餘|U+04F59余|U+09980馀|
+U+09935餵|U+05582喂|
U+09938餸|U+2980C𩠌|
U+099CE駎|U+299E8𩧨|
U+099DA駚|U+299EB𩧫|
@@ -174,6 +195,7 @@ U+09A1D騝|U+29A03𩨃|
U+09A1F騟|U+29A08𩨈|
U+09A2A騪|U+29A04𩨄|
U+09A4B驋|U+299EF𩧯|
+U+09B28鬨|U+054C4哄|
U+09B65魥|U+29F79𩽹|
U+09B95鮕|U+29F80𩾀|
U+09B9F鮟|U+29F7E𩽾|
@@ -186,15 +208,28 @@ U+09C06鰆|U+04CA0䲠|
U+09C0C鰌|U+04CA1䲡|
U+09C27鰧|U+04CA2䲢|
U+09C47鱇|U+29F8C𩾌|
+U+09C47鱇|U+29F8C𩾌|
U+09CFC鳼|U+2A243𪉃|
U+09D1C鴜|U+2A248𪉈|
U+09D32鴲|U+2A246𪉆|
U+09D5A鵚|U+2A24D𪉍|
+U+09D70鵰|U+096D5雕|U+05F6B彫|
U+09DD4鷔|U+2A251𪉑|
U+09DE8鷨|U+2A24A𪉊|
+U+09E7C鹼|U+078B1碱|U+07877硷|
U+09EA8麨|U+2A38A𪎊|
+U+09EAA麪|U+09762面|
+U+09EAB麫|U+09762面|
+U+09EAF麯|U+066F2曲|
U+09EB2麲|U+2A389𪎉|
U+09EB3麳|U+2A38C𪎌|
+U+09EB4麴|U+066F2曲|U+09EB4麴|
+U+09EF4黴|U+09709霉|
+U+09F15鼕|U+051AC冬|
+U+09F47齇|U+09F44齄|
+U+09F63齣|U+051FA出|
+U+21ED5𡻕|U+05C81岁|
+U+26A99𦪙|U+0447D䑽|
U+2895B𨥛|U+28C40𨱀|
U+289F1𨧱|U+28C4A𨱊|
U+28AD2𨫒|U+28C50𨱐|
@@ -250,30 +285,4 @@ U+2A106𪄆|U+2A254𪉔|
U+2A115𪄕|U+2A252𪉒|
U+2A1F3𪇳|U+2A255𪉕|
U+2A600𪘀|U+2A68F𪚏|
-U+2A62F𪘯|U+2A690𪚐|
-U+0617C慼|U+0621A戚|
-U+093DA鏚|U+0621A戚|
-U+07895碕|U+057FC埼|
-U+068CA棊|U+068CB棋|
-U+065C2旂|U+065D7旗|
-U+06371捱|U+06328挨|
-U+09061遡|U+06EAF溯|
-U+06CDD泝|U+06EAF溯|
-U+075E0痠|U+09178酸|
-U+07958祘|U+07B97算|
-U+05D57嵗|U+05C81岁|
-U+21ED5𡻕|U+05C81岁|
-U+07E50繐|U+07A57穗|
-U+098F1飱|U+098E7飧|
-U+06331挱|U+06332挲|
-U+07C11簑|U+084D1蓑|
-U+04308䌈|U+26216𦈖|
-U+06FBE澾|U+03CE0㳠|
-U+26A99𦪙|U+0447D䑽|
-U+07F4E罎|U+0575B坛|
-U+058DC壜|U+0575B坛|
-U+058B0墰|U+0575B坛|
-U+091A3醣|U+07CD6糖|
-U+0537D卽|U+05373即|
-U+065E3旣|U+065E2既|
-U+09EB4麴|U+066F2曲|U+09EB4麴| \ No newline at end of file
+U+2A62F𪘯|U+2A690𪚐| \ No newline at end of file
diff --git a/includes/zhtable/tradphrases.manual b/includes/zhtable/tradphrases.manual
index 4e9f7498..f3a95335 100644
--- a/includes/zhtable/tradphrases.manual
+++ b/includes/zhtable/tradphrases.manual
@@ -34,9 +34,9 @@
千隻
萬隻
億隻
-多只是
-多只需
-最多只
+最多
+至多
+頂多
多隻
0多隻
0多隻
@@ -46,6 +46,30 @@
千多隻
萬多隻
億多隻
+多只能
+多只可
+多只在
+多只有
+多只是
+多只需
+多只會
+大只能
+大只可
+大只在
+大只有
+大只是
+大只需
+大只會
+小只能
+小只可
+小只在
+小只有
+小只是
+小只需
+小只會
+隻身
+形單影隻
+首隻
數天後
幾天後
多天後
@@ -224,8 +248,6 @@
別著
刮著
千絲萬縷
-參与
-參与者
參合
參考價值
參與
@@ -266,7 +288,6 @@
斗轉參橫
旋繞著
板著臉
-標志著
正當著
沈著
沖著
@@ -340,10 +361,15 @@
雞絲麵
面朝著
面臨著
-顯著標志
颳著
髮絲
斷髮
+不斷發
+判斷發
+評斷發
+買斷發
+賣斷發
+打斷發
披頭散髮
髮禁
鬥著
@@ -375,6 +401,11 @@
錶帶
錶針
錶蒙子
+袋錶
+腕錶
+碼錶
+錶冠
+魔錶
彆口氣
彆強
皺彆
@@ -487,6 +518,7 @@
官地為寀
寮寀
蔘綏
+個人# “個人參數”不是“個人蔘數”
人蔘
蕭蔘
人參與
@@ -691,6 +723,17 @@
霽範
鴻範
沒樣範
+錢範
+銅範
+金範
+範金
+垂範
+範性形變
+範字
+有事之無範
+置言成範
+吾爲之範我馳驅
+天地為範
丰采
丰標不凡
丰神
@@ -710,6 +753,7 @@
複函數
複流
反複製
+複對數
顛覆
答覆
覆沒
@@ -835,6 +879,8 @@
西歷史
國歷史
國歷代
+國歷任
+國歷屆
新歷史
夏歷史
百花曆
@@ -877,6 +923,10 @@
儒略改革曆
希伯來曆
格里曆
+格里高利曆
+共和曆
+掛曆
+共和歷史
厤物之意
爰定祥厤
白黴
@@ -1043,6 +1093,10 @@
上簽字
上簽收
上簽寫
+下簽名
+下簽字
+下簽收
+下簽寫
犖确
磽确
确瘠
@@ -1071,6 +1125,7 @@
兀朮
白朮
蒼朮
+赤朮
朮赤
髼鬆
皮鬆
@@ -1085,6 +1140,8 @@
鬆寬
鬆氣
鬆一口氣
+鬆元音
+鬆喉
囉囉囌囌
囉囌
骨罈
@@ -1184,11 +1241,9 @@
松山庄
厂部
閤府
-分佈
佈道
剪綵
衝量
-韶山衝
衝車
書獃子
相干
@@ -1273,6 +1328,8 @@
杰特
李連杰
周杰倫
+杰倫
+姜文杰
稜鏡
稜角
稜台
@@ -1451,7 +1508,7 @@
於國
於潛縣
於焉
-於征
+於徵
離於
於畢
麗於
@@ -1740,6 +1797,7 @@
佔後
佔著
佔山
+馬占山
佔比
佔停車
佔哺乳
@@ -1807,11 +1865,14 @@
碰鐘
鳴鐘
晨鐘
+鐘體
飯後鐘
盜鐘
-當一天和尚撞一天鐘
+一天鐘
+撞鐘
殿鐘自鳴
天文鐘
+天文學鐘
洛鐘東應
亮鐘
郘鐘
@@ -1822,12 +1883,11 @@
擊鐘
警世鐘
竊鐘掩耳
-潛水鐘
琴鐘
見鐘不打
釁鐘
朝鐘
-撞木鐘
+木鐘
鐘不扣不鳴
鐘鳴
鐘塔
@@ -1837,7 +1897,7 @@
鐘形蟲
鐘乳洞
鐘乳石
-鐘在寺里
+鐘在寺裡
詩鐘
懸鐘
山崩鐘應
@@ -1845,13 +1905,230 @@
宗周鐘
塞耳盜鐘
二缶鐘惑
-一口鐘
+口鐘
+鐘的
+的鐘
+這鐘
叩鐘
音聲如鐘
應鐘
原子鐘
泳氣鐘
電子鐘
+電子鐘錶
+石英鐘錶
+石英鐘
+鐘錶王
+鐘律
+看鐘
+看錶
+看表面
+鐵鐘
+看下鐘
+看下錶
+瞅下鐘
+瞅下錶
+拿下鐘
+拿下錶
+鐘不敲不響
+對準鐘
+對準鐘錶
+對準錶
+鐘錶快
+鐘快
+錶快
+鐘錶慢
+鐘慢
+錶慢
+響鐘
+鐘敲
+大本鐘敲
+大笨鐘敲
+世紀鐘錶
+世紀鐘
+錶王
+鐘王
+鐘錶
+古鐘
+古鐘錶
+鐘面
+鐘表面
+南京鐘
+南京鐘錶
+造鐘錶
+造鐘
+九龍表行
+鐘錶行
+鐘行
+錶行
+小型鐘表面
+小型鐘面
+小型鐘錶
+小型鐘
+中型鐘表面
+中型鐘面
+中型鐘錶
+中型鐘
+大型鐘表面
+大型鐘面
+大型鐘錶
+大型鐘
+鐘匠
+深山何處鐘
+下課鐘
+上課鐘
+老爺鐘
+萬年曆錶
+個鐘
+個鐘錶
+喜歡鐘
+喜歡鐘錶
+喜歡錶
+大鐘
+佛鐘
+鐘壁
+鐘腰
+鐘口
+鐘身
+鐘模
+鐘頂
+鐘紐
+鐘座
+他鐘
+寺鐘
+座鐘
+盜鐘
+大笨鐘
+大本鐘
+鐘錶歷史
+錶的歷史
+鐘錶的歷史
+點多鐘
+點半鐘
+分多鐘
+刻多鐘
+分半鐘
+刻半鐘
+教學鐘
+操作鐘
+南屏晚鐘
+敲鐘
+瞧著鐘
+瞧著鐘錶
+瞧著錶
+警報鐘
+猶如鐘
+猶如鐘錶
+猶如錶
+舊鐘錶
+繁鐘
+四面鐘
+更鐘
+警示鐘
+鐘差
+任何鐘錶
+任何鐘
+任何錶
+選手表現
+選手表達
+選手表示
+選手表明
+選手表決
+分子鐘
+飛行鐘
+鐘罩
+主鐘差
+花鐘
+磬鐘
+主鐘曲線
+鐘速
+紅鐘
+各類鐘
+打著鐘
+鐘意
+衛星鐘
+該鐘
+錶轉
+鐘調
+調鐘錶
+調錶
+原鐘
+鐘錶速
+件鐘
+鐘發音
+逆鐘
+拂鐘無聲
+鐘不空則啞
+看著鐘錶
+看著鐘
+看著錶
+晚鐘
+潛水鐘錶
+潛水鐘
+潛水錶
+樂器鐘
+鐘左右
+埋頭尋鐘錶
+埋頭尋鐘
+埋頭尋錶
+鐘陳列
+驚鐘
+望著鐘錶
+望著鐘
+望著錶
+鐘錶停
+鐘停
+銫鐘
+數字鐘錶
+數字鐘
+數字錶
+顯示鐘錶
+顯示鐘
+顯示錶
+坐如鐘
+錶停
+西周鐘
+東周鐘
+錶速
+機械鐘錶
+機械鐘
+機械錶
+之鐘
+鐘形
+架鐘
+順鐘向
+逆鐘向
+遺傳鐘
+鬧錶
+華嚴鐘
+懷鐘
+生物鐘
+鐘錶的
+錶的嘀嗒
+的鐘錶
+嘀嗒的錶
+鐘好
+鐘太
+鐘不
+鐘有
+鐘盤
+鐘錶盤
+鐘沒
+鐘被
+制鐘
+布穀鳥鐘
+咕咕鐘
+拉克施爾德鐘
+鐘上
+鐘下
+摸鐘
+舊鐘
+舊錶
+台鐘
+鐘響
+叩鐘
+計時錶
+防水錶
射鵰
神鵰
神雕像
@@ -1859,7 +2136,6 @@
采石之戰
采石之役
聊齋志異
-不斷發
部落發
角落發
村落發
@@ -1883,7 +2159,6 @@
林郁方
讚歌
編餘
-三餘
餘墨
唾餘
餘韻
@@ -1977,6 +2252,42 @@
剩餘
餘生
有餘
+一餘
+二餘
+兩餘
+三餘
+四餘
+五餘
+六餘
+七餘
+八餘
+九餘
+十餘
+百餘
+千餘
+萬餘
+億餘
+兆餘
+0餘
+1餘
+2餘
+3餘
+4餘
+5餘
+6餘
+7餘
+8餘
+9餘
+0餘
+1餘
+2餘
+3餘
+4餘
+5餘
+6餘
+7餘
+8餘
+9餘
余光生
余光中
崑山
@@ -2079,7 +2390,6 @@
青蠅弔客
慶弔
形影相弔
-上弔
哀弔
一弔
唁弔
@@ -2163,8 +2473,12 @@
現於
較於
於之
-分佈於
+分布於
分散於
+優於
+早於
+晚於
+感於
鬼谷子
于美人
緊緻
@@ -2252,6 +2566,8 @@
困鬥
好勇鬥狠
爭奇鬥豔
+使其鬥
+鬥地主
石樑
木樑
藏歷史
@@ -2299,7 +2615,6 @@
餘8
餘9
餘0
-0餘
餘1
餘2
餘3
@@ -2310,7 +2625,6 @@
餘8
餘9
餘0
-0餘
餘數
其餘
尸居餘氣
@@ -2334,6 +2648,7 @@
病余
餘俗
餘倍
+同餘
大讚
唄讚
褒讚
@@ -2385,6 +2700,10 @@
三徵七辟
額徵
有徵
+有征服
+有征戰
+有征伐
+有征討
無徵不信
文徵明
徵跡
@@ -2458,12 +2777,17 @@
請君入甕
甕安
痊癒
+治癒
+病癒
+大病初癒
+癒合
槓桿
宣洩
圖鑑
諮詢
勳章
張勳
+趙治勳
殭屍
有栖川
兇惡
@@ -2485,7 +2809,6 @@
凝鍊
鍊貧
鍊度
-鍊金
鍊形
鍊師
鍊石
@@ -2496,6 +2819,7 @@
闖鍊
鍊汞
淬鍊
+鋼之鍊金術師
索馬里
范登堡
世田谷
@@ -2577,7 +2901,6 @@
齣子
齣兒
賣獃
-痴獃
發獃
大獃
獃獃
@@ -2611,28 +2934,54 @@
撲鼕鼕
普鼕鼕
鼕鼕鼓
+令人髮指
+開發
+剪其髮
+吐哺捉髮
+吐哺握髮
+含齒戴髮
+大金髮苔
+寸髮千金
+心長髮短
+戴髮含齒
+拔髮
+拔鬚
+揪髮
+揪鬚
+整髮用品
+斷髮文身
+滿頭洋髮
+燙一個髮
+燙一次髮
+燙個髮
+燙完髮
+燙次髮
+理一個髮
+理一次髮
+理個髮
+理完髮
+理次髮
+細如髮
+繫於一髮
+膚髮
+華髮
+蒼髮
+被髮佯狂
被髮入山
被髮左衽
-被髮佯狂
-被髮陽狂
被髮纓冠
+被髮陽狂
+身體髮膚
+髒髮
髮光可鑑
-髮際
+髮已霜白
髮油
+髮為血之本
髮網
髮踊沖冠
-大金髮苔
-戴髮含齒
-斷髮文身
-吐哺捉髮
-吐哺握髮
-令人髮指
-含齒戴髮
-華髮
+髮際
黃髮
-心長髮短
齒落髮白
-身體髮膚
剷頭
剷刈
口燥唇乾
@@ -2983,6 +3332,7 @@
蜡月
蜡祭
言云
+宜云
貴价
郁郁菲菲
馬杆
@@ -3000,4 +3350,552 @@
麴院
鼠麴草
不乾不淨
-生發生 \ No newline at end of file
+生發生
+必須
+須根據
+·范
+、剋制
+,剋制
+。剋制
+!剋制
+?剋制
+;剋制
+:剋制
+不剋制
+也剋制
+了剋制
+他剋制
+們剋制
+剋制不了
+剋制不住
+力剋制
+力求剋制
+可以剋制
+和剋制
+在剋制
+地剋制
+夠剋制
+她剋制
+你剋制
+您剋制
+就剋制
+彼此剋制
+得剋制
+快剋制
+想剋制
+意剋制
+應剋制
+我剋制
+才剋制
+於剋制
+易剋制
+無法剋制
+的剋制
+盡量剋制
+而剋制
+能剋制
+與剋制
+著剋制
+要剋制
+軍隊剋制
+空投佈雷
+火箭佈雷
+海灣佈雷
+空中佈雷
+海上佈雷
+佈雷的
+佈雷,
+佈雷、
+佈雷。
+佈雷;
+佈雷艦
+佈雷艇
+佈雷速度
+佈雷封鎖
+滿拚自盡
+拚生盡死
+拚卻
+拚老命
+拚絕
+成於思
+單單於
+積澱
+澱積
+澱北片
+澱解物
+澱謂之滓
+淺澱
+堙澱
+茂都澱
+並曰入澱
+澱乃不耕之地
+藍澱
+皆可作澱
+澱山
+澱澱
+掛鈎
+薴悴
+絡腮鬍
+落腮鬍
+山羊鬍
+幸運鬍
+刮鬍
+剃鬍
+吹鬍
+蓄鬍
+白鬍
+長鬍
+鬍髯
+髯鬍
+髭鬍
+鬚鬍
+范文瀾
+范文同
+范文正公
+范文程
+范文芳
+范文藤
+范文虎
+范文照
+發表
+乾重
+若干
+鈎心鬥角
+若干
+乾重
+全面包圍
+全面包裹
+機械系
+體系
+心理
+複分解
+鹰鵰
+叱咤903
+叱咤MY903
+叱咤My903
+叱咤樂壇
+叱咤咤
+叱咤叱咤叱咤咤
+叱咤叱叱咤
+正在叱咤
+空餘
+變髒
+天地志狼
+薴烯
+阿斯圖里亞斯
+雙折射
+心繫家
+心繫國
+心繫祖
+心繫北
+心繫京
+心繫南
+心繫西
+心繫東
+心繫四
+心繫川
+心繫浙
+心繫汶
+心繫廣
+心繫湖
+心繫山
+心繫台
+心繫江
+心繫昌
+心繫香
+心繫澳
+心繫港
+心繫泰
+心繫健
+心繫天
+心繫地
+心繫大
+心繫小
+心繫全
+心繫眾
+心繫奧
+心繫世
+心繫中
+心繫高
+心繫災
+心繫非
+心繫群
+心繫新
+心繫沈
+心繫唐
+心繫黃
+心繫乔
+心繫阮
+心繫父
+心繫母
+心繫病
+心繫故
+心繫哪
+心繫中
+心繫英
+心繫美
+心繫日
+心繫德
+心繫功
+心繫曉
+心繫神
+心繫萬
+心繫的
+心繫在
+心繫兩
+心繫社
+心繫曼
+心繫彼
+心繫風
+心繫募
+心繫一
+心繫何
+心繫困
+心繫輸
+心繫人
+心繫民
+心繫十
+心繫百
+心繫千
+心繫和
+心繫選
+心繫囑
+心繫我
+心繫你
+心繫您
+心繫他
+心繫她
+心繫它
+心繫伊
+心繫長
+心繫舞
+心繫蘭
+心繫五
+心繫生
+心繫婦
+心繫幼
+心繫茶
+心繫動
+心繫沙
+心繫林
+心繫摩
+心繫农
+心繫慈
+心繫麥
+心繫貧
+心繫富
+心繫遠
+心繫近
+心繫宣
+心繫傳
+心繫紅
+心繫老
+心繫重
+心繫震
+心繫妻
+心繫夫
+心繫女
+心繫子
+心繫著
+重回
+挑大樑
+扛大樑
+后豐
+製得
+限制
+控制
+製取
+第四出局
+心臟
+肝臟
+脾臟
+肺臟
+腎臟
+參與
+浮誇
+星巴克
+于謙
+于寘
+淳于
+于禁
+于敏中
+註:# 不作“注:”
+呆呆獸
+劃為# 不作“划為”
+併為一體
+併為一家
+一個# 避免“個裡”的錯誤
+兩個
+二個
+三個
+四個
+五個
+六個
+七個
+八個
+九個
+十個
+百個
+千個
+萬個
+億個
+兆個
+零個
+云:# 不作“雲:”
+電子表格
+雪裡紅
+雪裡蕻
+森林裡
+日子裡
+故事裡
+領域裡
+時間裡
+深淵裡
+醫院裡
+春假裡
+暑假裡
+秋假裡
+寒假裡
+春天裡
+夏天裡
+秋天裡
+冬天裡
+春日裡
+夏日裡
+秋日裡
+冬日裡
+嘴裡
+心裡
+皮裡陽秋
+肚裡
+苦裡
+裡勾外連
+裡面
+這裡
+中文裡
+山洞裡
+世界裡
+眼睛裡
+首發
+夸脫
+誰幹的
+鐘螺
+風采
+代碼表
+編碼表
+字碼表
+電碼表
+科斗
+佔領
+灕水
+點裡
+這只是
+葉叶琹
+胡子昂
+包括
+特别致
+分别致
+會上簽訂
+會上簽署
+周一 # (及以下)避免“周一齣版”的錯誤
+周二
+周三
+周四
+周五
+周六
+韶山沖
+總裁制
+于丹
+于樂
+于冕
+于軍
+于吉
+于堅
+于姓
+于娜
+于娟
+于山
+于帥
+于慧
+于振
+于敏
+于斌
+于晴
+于波
+于濤
+于衡
+于贈
+于越
+于靖
+于勒
+于格
+于仁泰
+于會泳
+于偉國
+于佳卉
+于光遠
+于克勒
+于凌奎
+于鳳至
+于化虎
+于占元
+于台煙
+于品海
+于國楨
+于大寶
+于天仁
+于子千
+于孔兼
+于學忠
+于家堡
+于小偉
+于小彤
+于山國
+于幼軍
+于廣洲
+于康震
+于式枚
+于從濂
+于德海
+于志寧
+于慎行
+于成龍
+于振武
+于明濤
+于是之
+于晨楠
+于根偉
+于樹潔
+于欣源
+于正昇
+于正昌
+于永波
+于漢超
+于江震
+于洪區
+于浩威
+于海洋
+于湘蘭
+于特森
+于玉立
+于秀敏
+于素秋
+于若木
+于蔭霖
+于西翰
+于遠偉
+于道泉
+于都縣
+于震寰
+于震環
+于非闇
+于風政
+于鳳桐
+于默奧
+于家堡
+于爾岑
+于默奧
+于貝爾
+于爾根
+于雙戈
+于里察
+于澤爾
+于斯塔德
+于斯達爾
+于爾里克
+于奇庫杜克
+于韋斯屈萊
+于克-蘭多縣
+于斯納爾斯貝里
+涂坤
+涂天相
+涂序瑄
+涂澤民
+涂紹煃
+涂羽卿
+涂逢年
+涂長望
+總裁制
+故云
+強制作用
+鬱南
+西米谷
+一出生
+二出生
+三出生
+四出生
+五出生
+六出生
+七出生
+八出生
+九出生
+十出生
+一出版
+二出版
+三出版
+四出版
+五出版
+六出版
+七出版
+八出版
+九出版
+十出版
+一出刊
+二出刊
+三出刊
+四出刊
+五出刊
+六出刊
+七出刊
+八出刊
+九出刊
+十出刊
+一出逃
+二出逃
+三出逃
+四出逃
+五出逃
+六出逃
+七出逃
+八出逃
+九出逃
+十出逃
+一出口
+二出口
+三出口
+四出口
+五出口
+六出口
+七出口
+八出口
+九出口
+十出口
+一出祁山
+二出祁山
+三出祁山
+四出祁山
+五出祁山
+六出祁山
+七出祁山
+八出祁山
+九出祁山
+十出祁山
+鬱林
+饑荒
+免徵
+亞美尼亞曆
+百科裡
+歷史裡
+戲裡
+作品裡
+專輯裡
+年代裡
+棺材裡
+注釋
+月面
+修杰楷
+學裡
+獄裡
+館裡
+系列裡
+村子裡
diff --git a/includes/zhtable/tradphrases_exclude.manual b/includes/zhtable/tradphrases_exclude.manual
index 9dadb6f7..5fec98b2 100644
--- a/includes/zhtable/tradphrases_exclude.manual
+++ b/includes/zhtable/tradphrases_exclude.manual
@@ -105,6 +105,7 @@
鬆毛蟲
鬆節油
濕地鬆
+尼克鬆
紮伊爾
阿布紮比
阿紮尼亞
@@ -143,6 +144,7 @@
彆腳
併力
併列
+併為
豐富多採
採採
尼採
@@ -266,4 +268,53 @@
朝乾夕惕
大曲酒
-神麴 \ No newline at end of file
+神麴
+便于
+偏于
+勇于
+居于
+常見于
+強加于
+從事于
+忙于
+敢于
+服務于
+服從于
+樂于
+歸罪于
+歸諸于
+活動于
+瀕于
+苦于
+莫過于
+處于
+適于
+乾和
+鉤
+高陞
+大胆
+託福
+繫系
+酰
+醯
+大樑
+光採
+鍾錶
+複原
+參与
+浮夸
+剋日
+羡
+旅游
+穀風
+復讎
+避暑山庄
+遊牧
+烟草
+征
+占領
+入夥
+懸挂
+註釋
+浮遊
+冶鍊