From 222b01f5169f1c7e69762e0e8904c24f78f71882 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 28 Jul 2010 11:52:48 +0200 Subject: update to MediaWiki 1.16.0 --- .gitignore | 1 + AdminSettings.sample | 32 - CREDITS | 41 +- FAQ | 7 +- HISTORY | 410 +- Makefile | 28 - RELEASE-NOTES | 1376 ++++-- StartProfiler.php | 22 - StartProfiler.sample | 23 + UPGRADE | 23 +- api.php | 87 +- cache/.htaccess | 1 + config/Installer.php | 2350 +++++++++++ config/index.php | 2191 +--------- config/index.php5 | 2 - docs/design.txt | 2 +- docs/distributors.txt | 192 + docs/export-0.4.xsd | 212 + docs/export-demo.xml | 21 +- docs/hooks.txt | 755 +++- docs/maintenance.txt | 57 + docs/memcached.txt | 15 +- docs/php-memcached/ChangeLog | 65 +- docs/php-memcached/README | 1 + docs/scripts.txt | 7 +- docs/skin.txt | 2 +- docs/upload.txt | 42 +- extensions/FluxBBAuthPlugin.php | 15 +- img_auth.php | 128 +- includes/AjaxDispatcher.php | 35 +- includes/AjaxFunctions.php | 96 +- includes/AjaxResponse.php | 44 +- includes/Article.php | 2217 ++++++---- includes/AuthPlugin.php | 47 +- includes/AutoLoader.php | 217 +- includes/Autopromote.php | 7 +- includes/BacklinkCache.php | 73 +- includes/BagOStuff.php | 660 ++- includes/Block.php | 185 +- includes/CacheDependency.php | 43 +- includes/Category.php | 80 +- includes/CategoryPage.php | 233 +- includes/Categoryfinder.php | 142 +- includes/Cdb.php | 149 + includes/Cdb_PHP.php | 374 ++ includes/ChangeTags.php | 98 +- includes/ChangesFeed.php | 68 +- includes/ChangesList.php | 519 ++- includes/ConfEditor.php | 1058 +++++ includes/Credits.php | 70 +- includes/DatabaseFunctions.php | 2 +- includes/DefaultSettings.php | 850 +++- includes/Defines.php | 5 +- includes/DjVuImage.php | 26 +- includes/DoubleRedirectJob.php | 12 +- includes/EditPage.php | 1575 +++---- includes/Exception.php | 18 +- includes/Exif.php | 18 +- includes/Export.php | 73 +- includes/ExternalStore.php | 48 +- includes/ExternalStoreDB.php | 49 +- includes/ExternalStoreHttp.php | 18 +- includes/ExternalUser.php | 304 ++ includes/FakeTitle.php | 83 +- includes/Feed.php | 111 +- includes/FeedUtils.php | 76 +- includes/FileDeleteForm.php | 60 +- includes/FileRevertForm.php | 13 +- includes/FileStore.php | 360 -- includes/ForkController.php | 24 +- includes/GlobalFunctions.php | 847 ++-- includes/HTMLCacheUpdate.php | 143 +- includes/HTMLFileCache.php | 21 +- includes/HTMLForm.php | 1391 +++++++ includes/HistoryPage.php | 730 ++++ includes/Hooks.php | 80 +- includes/Html.php | 539 +++ includes/HttpFunctions.php | 929 ++++- includes/IP.php | 160 +- includes/ImageFunctions.php | 6 + includes/ImageGallery.php | 37 +- includes/ImagePage.php | 361 +- includes/ImageQueryPage.php | 18 +- includes/Import.php | 63 +- includes/Interwiki.php | 133 +- includes/JSMin.php | 290 ++ includes/JobQueue.php | 42 +- includes/Licenses.php | 79 +- includes/LinkCache.php | 22 +- includes/LinkFilter.php | 84 +- includes/Linker.php | 1135 +++-- includes/LinksUpdate.php | 7 + includes/LocalisationCache.php | 999 +++++ includes/LogEventsList.php | 628 ++- includes/LogPage.php | 115 +- includes/MagicWord.php | 91 +- includes/Math.php | 39 +- includes/MediaTransformOutput.php | 30 +- includes/MessageCache.php | 295 +- includes/MimeMagic.php | 40 +- includes/Namespace.php | 47 +- includes/ObjectCache.php | 13 +- includes/OutputHandler.php | 22 +- includes/OutputPage.php | 1744 +++++--- includes/PageHistory.php | 630 --- includes/Pager.php | 213 +- includes/PatrolLog.php | 36 +- includes/PoolCounter.php | 64 + includes/Preferences.php | 1389 +++++++ includes/PrefixSearch.php | 53 +- includes/Profiler.php | 15 +- includes/ProfilerSimpleText.php | 4 + includes/ProtectionForm.php | 104 +- includes/ProxyTools.php | 29 +- includes/QueryPage.php | 174 +- includes/RawPage.php | 35 +- includes/RecentChange.php | 273 +- includes/RefreshLinksJob.php | 3 +- includes/Revision.php | 356 +- includes/Sanitizer.php | 392 +- includes/SearchEngine.php | 1205 ------ includes/SearchIBM_DB2.php | 247 -- includes/SearchMySQL.php | 270 -- includes/SearchMySQL4.php | 34 - includes/SearchOracle.php | 244 -- includes/SearchPostgres.php | 257 -- includes/SearchUpdate.php | 113 - includes/Setup.php | 100 +- includes/SiteConfiguration.php | 17 +- includes/SiteStats.php | 168 +- includes/Skin.php | 1064 +++-- includes/SkinTemplate.php | 593 +-- includes/SpecialPage.php | 104 +- includes/SquidPurgeClient.php | 380 ++ includes/SquidUpdate.php | 137 +- includes/Status.php | 16 +- includes/StreamFile.php | 7 +- includes/StubObject.php | 16 +- includes/Title.php | 740 ++-- includes/User.php | 829 ++-- includes/UserMailer.php | 19 +- includes/UserRightsProxy.php | 73 +- includes/WatchedItem.php | 4 +- includes/WatchlistEditor.php | 37 +- includes/WebRequest.php | 190 +- includes/WebResponse.php | 30 +- includes/WebStart.php | 7 +- includes/Wiki.php | 96 +- includes/WikiMap.php | 161 + includes/Xml.php | 75 +- includes/ZhConversion.php | 1781 ++++++-- includes/api/ApiBase.php | 955 +++-- includes/api/ApiBlock.php | 105 +- includes/api/ApiDelete.php | 203 +- includes/api/ApiDisabled.php | 23 +- includes/api/ApiEditPage.php | 385 +- includes/api/ApiEmailUser.php | 34 +- includes/api/ApiExpandTemplates.php | 17 +- includes/api/ApiFeedWatchlist.php | 80 +- includes/api/ApiFormatBase.php | 115 +- includes/api/ApiFormatDbg.php | 18 +- includes/api/ApiFormatJson.php | 44 +- includes/api/ApiFormatJson_json.php | 861 ---- includes/api/ApiFormatPhp.php | 12 +- includes/api/ApiFormatRaw.php | 28 +- includes/api/ApiFormatTxt.php | 18 +- includes/api/ApiFormatWddx.php | 77 +- includes/api/ApiFormatXml.php | 117 +- includes/api/ApiFormatYaml.php | 12 +- includes/api/ApiFormatYaml_spyc.php | 98 +- includes/api/ApiHelp.php | 12 +- includes/api/ApiImport.php | 98 +- includes/api/ApiLogin.php | 99 +- includes/api/ApiLogout.php | 12 +- includes/api/ApiMain.php | 347 +- includes/api/ApiMove.php | 159 +- includes/api/ApiOpenSearch.php | 38 +- includes/api/ApiPageSet.php | 257 +- includes/api/ApiParamInfo.php | 134 +- includes/api/ApiParse.php | 194 +- includes/api/ApiPatrol.php | 50 +- includes/api/ApiProtect.php | 146 +- includes/api/ApiPurge.php | 41 +- includes/api/ApiQuery.php | 243 +- includes/api/ApiQueryAllCategories.php | 92 +- includes/api/ApiQueryAllLinks.php | 135 +- includes/api/ApiQueryAllUsers.php | 148 +- includes/api/ApiQueryAllimages.php | 121 +- includes/api/ApiQueryAllmessages.php | 118 +- includes/api/ApiQueryAllpages.php | 162 +- includes/api/ApiQueryBacklinks.php | 338 +- includes/api/ApiQueryBase.php | 196 +- includes/api/ApiQueryBlocks.php | 205 +- includes/api/ApiQueryCategories.php | 179 +- includes/api/ApiQueryCategoryInfo.php | 66 +- includes/api/ApiQueryCategoryMembers.php | 186 +- includes/api/ApiQueryDeletedrevs.php | 257 +- includes/api/ApiQueryDisabled.php | 12 +- includes/api/ApiQueryDuplicateFiles.php | 97 +- includes/api/ApiQueryExtLinksUsage.php | 125 +- includes/api/ApiQueryExternalLinks.php | 50 +- includes/api/ApiQueryImageInfo.php | 229 +- includes/api/ApiQueryImages.php | 101 +- includes/api/ApiQueryInfo.php | 447 +- includes/api/ApiQueryLangLinks.php | 75 +- includes/api/ApiQueryLinks.php | 120 +- includes/api/ApiQueryLogEvents.php | 274 +- includes/api/ApiQueryProtectedTitles.php | 112 +- includes/api/ApiQueryRandom.php | 82 +- includes/api/ApiQueryRecentChanges.php | 383 +- includes/api/ApiQueryRevisions.php | 435 +- includes/api/ApiQuerySearch.php | 129 +- includes/api/ApiQuerySiteinfo.php | 198 +- includes/api/ApiQueryTags.php | 181 + includes/api/ApiQueryUserContributions.php | 305 +- includes/api/ApiQueryUserInfo.php | 99 +- includes/api/ApiQueryUsers.php | 202 +- includes/api/ApiQueryWatchlist.php | 316 +- includes/api/ApiQueryWatchlistRaw.php | 118 +- includes/api/ApiResult.php | 168 +- includes/api/ApiRollback.php | 76 +- includes/api/ApiUnblock.php | 59 +- includes/api/ApiUndelete.php | 87 +- includes/api/ApiUpload.php | 325 ++ includes/api/ApiUserrights.php | 128 + includes/api/ApiWatch.php | 46 +- includes/cbt/CBTCompiler.php | 366 -- includes/cbt/CBTProcessor.php | 539 --- includes/cbt/README | 108 - includes/db/Database.php | 812 ++-- includes/db/DatabaseIbm_db2.php | 908 ++-- includes/db/DatabaseMssql.php | 86 +- includes/db/DatabaseMysql.php | 453 ++ includes/db/DatabaseOracle.php | 1049 +++-- includes/db/DatabasePostgres.php | 184 +- includes/db/DatabaseSqlite.php | 432 +- includes/db/LBFactory.php | 14 +- includes/db/LoadBalancer.php | 21 +- includes/diff/DifferenceEngine.php | 933 ----- includes/diff/DifferenceInterface.php | 1021 +++++ includes/diff/HTMLDiff.php | 1009 ----- includes/diff/Nodes.php | 439 -- includes/extauth/Hardcoded.php | 79 + includes/extauth/MediaWiki.php | 141 + includes/extauth/vB.php | 140 + includes/filerepo/ArchivedFile.php | 68 +- includes/filerepo/FSRepo.php | 103 +- includes/filerepo/File.php | 76 +- includes/filerepo/FileCache.php | 156 - includes/filerepo/FileRepo.php | 175 +- includes/filerepo/ForeignAPIFile.php | 11 +- includes/filerepo/ForeignAPIRepo.php | 114 +- includes/filerepo/ForeignDBFile.php | 10 - includes/filerepo/ForeignDBRepo.php | 15 + includes/filerepo/ForeignDBViaLBRepo.php | 15 + includes/filerepo/Image.php | 4 +- includes/filerepo/LocalFile.php | 224 +- includes/filerepo/LocalRepo.php | 97 +- includes/filerepo/NullRepo.php | 8 +- includes/filerepo/OldLocalFile.php | 24 +- includes/filerepo/RepoGroup.php | 124 +- includes/json/FormatJson.php | 32 + includes/json/Services_JSON.php | 875 ++++ includes/media/Bitmap.php | 121 +- includes/media/DjVu.php | 48 +- includes/media/GIF.php | 72 + includes/media/GIFMetadataExtractor.php | 175 + includes/media/Generic.php | 44 +- includes/media/SVG.php | 2 - includes/memcached-client.php | 1990 +++++---- includes/mime.types | 6 +- includes/normal/RandomTest.php | 2 +- includes/normal/Utf8CaseGenerate.php | 2 +- includes/normal/Utf8Test.php | 2 +- includes/normal/UtfNormal.php | 8 +- includes/normal/UtfNormalData.inc | 10 +- includes/normal/UtfNormalDataK.inc | 4 +- includes/normal/UtfNormalGenerate.php | 8 +- includes/parser/CoreParserFunctions.php | 148 +- includes/parser/CoreTagHooks.php | 49 + includes/parser/DateFormatter.php | 10 +- includes/parser/LinkHolderArray.php | 3 + includes/parser/Parser.php | 1121 +++-- includes/parser/ParserCache.php | 75 +- includes/parser/ParserOptions.php | 27 +- includes/parser/ParserOutput.php | 31 +- includes/parser/Preprocessor.php | 15 + includes/parser/Preprocessor_DOM.php | 31 +- includes/parser/Preprocessor_Hash.php | 18 +- includes/search/SearchEngine.php | 1248 ++++++ includes/search/SearchIBM_DB2.php | 224 + includes/search/SearchMySQL.php | 412 ++ includes/search/SearchMySQL4.php | 34 + includes/search/SearchOracle.php | 268 ++ includes/search/SearchPostgres.php | 246 ++ includes/search/SearchSqlite.php | 344 ++ includes/search/SearchUpdate.php | 113 + includes/specials/SpecialActiveusers.php | 195 + includes/specials/SpecialAllmessages.php | 581 ++- includes/specials/SpecialAllpages.php | 180 +- includes/specials/SpecialAncientpages.php | 28 +- includes/specials/SpecialBlankpage.php | 19 +- includes/specials/SpecialBlockip.php | 433 +- includes/specials/SpecialBooksources.php | 4 +- includes/specials/SpecialBrokenRedirects.php | 42 +- includes/specials/SpecialCategories.php | 13 +- includes/specials/SpecialConfirmemail.php | 14 +- includes/specials/SpecialContributions.php | 318 +- includes/specials/SpecialDeletedContributions.php | 243 +- includes/specials/SpecialDisambiguations.php | 2 +- includes/specials/SpecialDoubleRedirects.php | 28 +- includes/specials/SpecialEmailuser.php | 23 +- includes/specials/SpecialExport.php | 69 +- includes/specials/SpecialFewestrevisions.php | 21 +- includes/specials/SpecialFileDuplicateSearch.php | 23 +- includes/specials/SpecialFilepath.php | 4 +- includes/specials/SpecialImport.php | 15 +- includes/specials/SpecialIpblocklist.php | 64 +- includes/specials/SpecialLinkSearch.php | 18 +- includes/specials/SpecialListUserRestrictions.php | 162 - includes/specials/SpecialListfiles.php | 22 +- includes/specials/SpecialListgrouprights.php | 74 +- includes/specials/SpecialListredirects.php | 9 +- includes/specials/SpecialListusers.php | 34 +- includes/specials/SpecialLockdb.php | 6 +- includes/specials/SpecialLog.php | 12 +- includes/specials/SpecialMIMEsearch.php | 20 +- includes/specials/SpecialMergeHistory.php | 38 +- includes/specials/SpecialMostlinked.php | 37 +- includes/specials/SpecialMostlinkedcategories.php | 2 +- includes/specials/SpecialMostlinkedtemplates.php | 33 +- includes/specials/SpecialMostrevisions.php | 9 +- includes/specials/SpecialMovepage.php | 138 +- includes/specials/SpecialNewimages.php | 81 +- includes/specials/SpecialNewpages.php | 38 +- includes/specials/SpecialPopularpages.php | 12 +- includes/specials/SpecialPreferences.php | 1315 +----- includes/specials/SpecialPrefixindex.php | 28 +- includes/specials/SpecialProtectedpages.php | 47 +- includes/specials/SpecialProtectedtitles.php | 6 +- includes/specials/SpecialRandompage.php | 61 +- includes/specials/SpecialRandomredirect.php | 5 +- includes/specials/SpecialRecentchanges.php | 112 +- includes/specials/SpecialRecentchangeslinked.php | 72 +- includes/specials/SpecialRemoveRestrictions.php | 10 +- includes/specials/SpecialResetpass.php | 76 +- includes/specials/SpecialRestrictUser.php | 190 - includes/specials/SpecialRevisiondelete.php | 2817 +++++++------ includes/specials/SpecialSearch.php | 1419 +++---- includes/specials/SpecialShortpages.php | 11 +- includes/specials/SpecialSpecialpages.php | 6 +- includes/specials/SpecialStatistics.php | 70 +- includes/specials/SpecialTags.php | 12 +- .../specials/SpecialUncategorizedtemplates.php | 2 - includes/specials/SpecialUndelete.php | 366 +- includes/specials/SpecialUnlockdb.php | 6 +- includes/specials/SpecialUnusedcategories.php | 2 +- includes/specials/SpecialUnusedimages.php | 16 +- includes/specials/SpecialUnusedtemplates.php | 13 +- includes/specials/SpecialUnwatchedpages.php | 12 +- includes/specials/SpecialUpload.php | 2402 ++++------- includes/specials/SpecialUploadMogile.php | 135 - includes/specials/SpecialUserlogin.php | 215 +- includes/specials/SpecialUserlogout.php | 10 + includes/specials/SpecialUserrights.php | 390 +- includes/specials/SpecialVersion.php | 312 +- includes/specials/SpecialWantedcategories.php | 39 +- includes/specials/SpecialWantedfiles.php | 59 +- includes/specials/SpecialWantedpages.php | 93 +- includes/specials/SpecialWantedtemplates.php | 59 +- includes/specials/SpecialWatchlist.php | 120 +- includes/specials/SpecialWhatlinkshere.php | 84 +- includes/specials/SpecialWithoutinterwiki.php | 11 +- includes/templates/NoLocalSettings.php | 4 +- includes/templates/PHP4.php | 4 +- includes/templates/Userlogin.php | 111 +- includes/upload/UploadBase.php | 1091 +++++ includes/upload/UploadFromFile.php | 32 + includes/upload/UploadFromStash.php | 84 + includes/upload/UploadFromUrl.php | 137 + includes/zhtable/Makefile | 2 +- includes/zhtable/Makefile.py | 83 +- includes/zhtable/simp2trad.manual | 459 +- includes/zhtable/simpphrases.manual | 122 +- includes/zhtable/simpphrases_exclude.manual | 4 +- includes/zhtable/toCN.manual | 12 +- includes/zhtable/toHK.manual | 206 +- includes/zhtable/toSimp.manual | 25 +- includes/zhtable/toTW.manual | 59 +- includes/zhtable/toTrad.manual | 77 +- includes/zhtable/trad2simp.manual | 265 +- includes/zhtable/tradphrases.manual | 972 ++++- includes/zhtable/tradphrases_exclude.manual | 53 +- index.php | 7 +- install-utils.inc | 213 - languages/Language.php | 1075 +++-- languages/LanguageConverter.php | 1533 ++++--- languages/Names.php | 84 +- languages/classes/LanguageAm.php | 16 + languages/classes/LanguageAr.php | 31 +- languages/classes/LanguageBe.php | 3 +- languages/classes/LanguageBe_tarask.php | 3 +- languages/classes/LanguageBh.php | 16 + languages/classes/LanguageBs.php | 2 + languages/classes/LanguageCy.php | 3 + languages/classes/LanguageEo.php | 3 +- languages/classes/LanguageGa.php | 16 + languages/classes/LanguageGan.php | 30 +- languages/classes/LanguageGd.php | 35 + languages/classes/LanguageHi.php | 16 + languages/classes/LanguageHr.php | 2 + languages/classes/LanguageJa.php | 27 +- languages/classes/LanguageKk.php | 13 +- languages/classes/LanguageKu.php | 10 - languages/classes/LanguageLn.php | 18 + languages/classes/LanguageLv.php | 3 + languages/classes/LanguageMg.php | 16 + languages/classes/LanguageMk.php | 21 + languages/classes/LanguageMl.php | 22 + languages/classes/LanguageMo.php | 23 + languages/classes/LanguageNso.php | 16 + languages/classes/LanguagePl.php | 8 + languages/classes/LanguageRo.php | 23 + languages/classes/LanguageRu.php | 2 + languages/classes/LanguageSe.php | 22 + languages/classes/LanguageSh.php | 29 + languages/classes/LanguageSma.php | 22 + languages/classes/LanguageSr.php | 36 +- languages/classes/LanguageTi.php | 16 + languages/classes/LanguageTl.php | 16 + languages/classes/LanguageTr.php | 2 +- languages/classes/LanguageUk.php | 2 + languages/classes/LanguageWa.php | 12 +- languages/classes/LanguageYue.php | 31 +- languages/classes/LanguageZh.php | 39 +- languages/classes/LanguageZh_hans.php | 43 +- languages/messages/MessagesAb.php | 45 +- languages/messages/MessagesAce.php | 107 +- languages/messages/MessagesAf.php | 1034 +++-- languages/messages/MessagesAk.php | 2 + languages/messages/MessagesAln.php | 457 +- languages/messages/MessagesAls.php | 3 + languages/messages/MessagesAm.php | 268 +- languages/messages/MessagesAn.php | 987 +++-- languages/messages/MessagesAng.php | 260 +- languages/messages/MessagesAr.php | 1175 +++--- languages/messages/MessagesArc.php | 437 +- languages/messages/MessagesArn.php | 89 +- languages/messages/MessagesArz.php | 1290 +++--- languages/messages/MessagesAs.php | 394 +- languages/messages/MessagesAst.php | 376 +- languages/messages/MessagesAv.php | 17 +- languages/messages/MessagesAvk.php | 217 +- languages/messages/MessagesAy.php | 22 +- languages/messages/MessagesAz.php | 533 ++- languages/messages/MessagesBa.php | 608 ++- languages/messages/MessagesBar.php | 110 +- languages/messages/MessagesBat_smg.php | 232 +- languages/messages/MessagesBcc.php | 713 ++-- languages/messages/MessagesBcl.php | 423 +- languages/messages/MessagesBe.php | 509 +-- languages/messages/MessagesBe_tarask.php | 964 +++-- languages/messages/MessagesBe_x_old.php | 3 + languages/messages/MessagesBg.php | 923 +++-- languages/messages/MessagesBh.php | 119 +- languages/messages/MessagesBi.php | 10 +- languages/messages/MessagesBm.php | 15 +- languages/messages/MessagesBn.php | 612 +-- languages/messages/MessagesBo.php | 24 +- languages/messages/MessagesBpy.php | 359 +- languages/messages/MessagesBqi.php | 104 +- languages/messages/MessagesBr.php | 971 +++-- languages/messages/MessagesBs.php | 1054 +++-- languages/messages/MessagesBug.php | 72 +- languages/messages/MessagesCa.php | 958 +++-- languages/messages/MessagesCbk_zam.php | 9 +- languages/messages/MessagesCdo.php | 213 +- languages/messages/MessagesCe.php | 200 +- languages/messages/MessagesCeb.php | 485 ++- languages/messages/MessagesCh.php | 197 +- languages/messages/MessagesChr.php | 14 +- languages/messages/MessagesCkb.php | 12 + languages/messages/MessagesCkb_arab.php | 2997 +++++++++++++ languages/messages/MessagesCkb_latn.php | 12 + languages/messages/MessagesCo.php | 55 +- languages/messages/MessagesCps.php | 946 +++++ languages/messages/MessagesCrh.php | 3 + languages/messages/MessagesCrh_cyrl.php | 555 +-- languages/messages/MessagesCrh_latn.php | 556 +-- languages/messages/MessagesCs.php | 1116 +++-- languages/messages/MessagesCsb.php | 117 +- languages/messages/MessagesCu.php | 125 +- languages/messages/MessagesCv.php | 224 +- languages/messages/MessagesCy.php | 1039 +++-- languages/messages/MessagesDa.php | 968 +++-- languages/messages/MessagesDe.php | 1084 +++-- languages/messages/MessagesDe_ch.php | 5 +- languages/messages/MessagesDe_formal.php | 125 +- languages/messages/MessagesDiq.php | 933 +++-- languages/messages/MessagesDk.php | 3 + languages/messages/MessagesDsb.php | 963 +++-- languages/messages/MessagesDv.php | 50 +- languages/messages/MessagesDz.php | 95 +- languages/messages/MessagesEe.php | 134 +- languages/messages/MessagesEl.php | 974 +++-- languages/messages/MessagesEml.php | 12 +- languages/messages/MessagesEn.php | 1192 ++++-- languages/messages/MessagesEnRTL.php | 3 + languages/messages/MessagesEn_gb.php | 2 +- languages/messages/MessagesEo.php | 964 +++-- languages/messages/MessagesEs.php | 1112 +++-- languages/messages/MessagesEt.php | 1134 +++-- languages/messages/MessagesEu.php | 922 ++-- languages/messages/MessagesExt.php | 327 +- languages/messages/MessagesFa.php | 984 +++-- languages/messages/MessagesFf.php | 2 + languages/messages/MessagesFi.php | 956 +++-- languages/messages/MessagesFiu_vro.php | 3 + languages/messages/MessagesFj.php | 18 +- languages/messages/MessagesFo.php | 151 +- languages/messages/MessagesFr.php | 1186 +++--- languages/messages/MessagesFrc.php | 245 +- languages/messages/MessagesFrp.php | 1235 ++++-- languages/messages/MessagesFrr.php | 1213 ++++++ languages/messages/MessagesFur.php | 603 +-- languages/messages/MessagesFy.php | 496 ++- languages/messages/MessagesGa.php | 438 +- languages/messages/MessagesGag.php | 184 +- languages/messages/MessagesGan.php | 2179 +--------- languages/messages/MessagesGan_hans.php | 347 +- languages/messages/MessagesGan_hant.php | 372 +- languages/messages/MessagesGd.php | 151 +- languages/messages/MessagesGl.php | 1090 +++-- languages/messages/MessagesGlk.php | 8 +- languages/messages/MessagesGn.php | 43 +- languages/messages/MessagesGot.php | 44 +- languages/messages/MessagesGrc.php | 746 ++-- languages/messages/MessagesGsw.php | 966 +++-- languages/messages/MessagesGu.php | 420 +- languages/messages/MessagesGv.php | 175 +- languages/messages/MessagesHa.php | 721 ++++ languages/messages/MessagesHak.php | 272 +- languages/messages/MessagesHaw.php | 287 +- languages/messages/MessagesHe.php | 1321 +++--- languages/messages/MessagesHi.php | 758 ++-- languages/messages/MessagesHif.php | 3 + languages/messages/MessagesHif_latn.php | 949 +++-- languages/messages/MessagesHil.php | 52 +- languages/messages/MessagesHr.php | 967 +++-- languages/messages/MessagesHsb.php | 951 +++-- languages/messages/MessagesHt.php | 178 +- languages/messages/MessagesHu.php | 1022 +++-- languages/messages/MessagesHy.php | 699 ++-- languages/messages/MessagesIa.php | 960 +++-- languages/messages/MessagesId.php | 1366 +++--- languages/messages/MessagesIe.php | 36 +- languages/messages/MessagesIg.php | 274 +- languages/messages/MessagesIke_cans.php | 57 +- languages/messages/MessagesIke_latn.php | 53 +- languages/messages/MessagesIlo.php | 225 +- languages/messages/MessagesInh.php | 28 +- languages/messages/MessagesIo.php | 496 ++- languages/messages/MessagesIs.php | 501 +-- languages/messages/MessagesIt.php | 1006 +++-- languages/messages/MessagesIu.php | 3 + languages/messages/MessagesJa.php | 1096 +++-- languages/messages/MessagesJbo.php | 25 +- languages/messages/MessagesJut.php | 130 +- languages/messages/MessagesJv.php | 733 ++-- languages/messages/MessagesKa.php | 958 +++-- languages/messages/MessagesKaa.php | 248 +- languages/messages/MessagesKab.php | 238 +- languages/messages/MessagesKg.php | 12 +- languages/messages/MessagesKiu.php | 1411 +++++++ languages/messages/MessagesKk_arab.php | 564 ++- languages/messages/MessagesKk_cn.php | 3 + languages/messages/MessagesKk_cyrl.php | 595 +-- languages/messages/MessagesKk_kz.php | 3 + languages/messages/MessagesKk_latn.php | 562 ++- languages/messages/MessagesKk_tr.php | 3 + languages/messages/MessagesKl.php | 197 +- languages/messages/MessagesKm.php | 943 +++-- languages/messages/MessagesKn.php | 389 +- languages/messages/MessagesKo.php | 1029 +++-- languages/messages/MessagesKo_kp.php | 33 + languages/messages/MessagesKoi.php | 630 +++ languages/messages/MessagesKrc.php | 3219 ++++++++++++++ languages/messages/MessagesKri.php | 41 +- languages/messages/MessagesKrj.php | 33 +- languages/messages/MessagesKs.php | 3 + languages/messages/MessagesKsh.php | 975 +++-- languages/messages/MessagesKu.php | 3 + languages/messages/MessagesKu_arab.php | 1065 +---- languages/messages/MessagesKu_latn.php | 239 +- languages/messages/MessagesKv.php | 4 +- languages/messages/MessagesKw.php | 355 +- languages/messages/MessagesKy.php | 78 +- languages/messages/MessagesLa.php | 510 +-- languages/messages/MessagesLad.php | 112 +- languages/messages/MessagesLb.php | 1015 +++-- languages/messages/MessagesLbe.php | 13 +- languages/messages/MessagesLez.php | 15 +- languages/messages/MessagesLfn.php | 148 +- languages/messages/MessagesLg.php | 76 +- languages/messages/MessagesLi.php | 745 ++-- languages/messages/MessagesLij.php | 192 +- languages/messages/MessagesLld.php | 9 - languages/messages/MessagesLmo.php | 425 +- languages/messages/MessagesLn.php | 88 +- languages/messages/MessagesLo.php | 97 +- languages/messages/MessagesLoz.php | 304 +- languages/messages/MessagesLt.php | 989 +++-- languages/messages/MessagesLtg.php | 746 ++++ languages/messages/MessagesLv.php | 472 ++- languages/messages/MessagesLzh.php | 891 ++-- languages/messages/MessagesLzz.php | 78 +- languages/messages/MessagesMai.php | 11 +- languages/messages/MessagesMap_bms.php | 4 +- languages/messages/MessagesMdf.php | 387 +- languages/messages/MessagesMg.php | 876 ++-- languages/messages/MessagesMhr.php | 260 +- languages/messages/MessagesMi.php | 12 +- languages/messages/MessagesMk.php | 974 +++-- languages/messages/MessagesMl.php | 1336 +++--- languages/messages/MessagesMn.php | 556 +-- languages/messages/MessagesMo.php | 13 +- languages/messages/MessagesMr.php | 419 +- languages/messages/MessagesMrj.php | 716 ++++ languages/messages/MessagesMs.php | 762 ++-- languages/messages/MessagesMt.php | 718 ++-- languages/messages/MessagesMwl.php | 184 +- languages/messages/MessagesMy.php | 43 +- languages/messages/MessagesMyv.php | 533 +-- languages/messages/MessagesMzn.php | 301 +- languages/messages/MessagesNa.php | 14 +- languages/messages/MessagesNah.php | 324 +- languages/messages/MessagesNan.php | 154 +- languages/messages/MessagesNap.php | 54 +- languages/messages/MessagesNb.php | 3 + languages/messages/MessagesNds.php | 691 ++- languages/messages/MessagesNds_nl.php | 1076 +++-- languages/messages/MessagesNe.php | 630 +-- languages/messages/MessagesNew.php | 47 +- languages/messages/MessagesNiu.php | 24 +- languages/messages/MessagesNl.php | 1032 +++-- languages/messages/MessagesNn.php | 931 +++-- languages/messages/MessagesNo.php | 985 +++-- languages/messages/MessagesNov.php | 54 +- languages/messages/MessagesNso.php | 221 +- languages/messages/MessagesNv.php | 41 +- languages/messages/MessagesOc.php | 1074 +++-- languages/messages/MessagesOr.php | 4 +- languages/messages/MessagesOs.php | 289 +- languages/messages/MessagesPa.php | 218 +- languages/messages/MessagesPag.php | 66 +- languages/messages/MessagesPam.php | 309 +- languages/messages/MessagesPap.php | 43 +- languages/messages/MessagesPcd.php | 856 ++++ languages/messages/MessagesPdc.php | 254 +- languages/messages/MessagesPdt.php | 80 +- languages/messages/MessagesPfl.php | 41 +- languages/messages/MessagesPih.php | 4 +- languages/messages/MessagesPl.php | 975 +++-- languages/messages/MessagesPms.php | 1006 +++-- languages/messages/MessagesPnb.php | 269 +- languages/messages/MessagesPnt.php | 286 +- languages/messages/MessagesPrg.php | 2846 +++++++++++++ languages/messages/MessagesPs.php | 472 ++- languages/messages/MessagesPt.php | 1262 +++--- languages/messages/MessagesPt_br.php | 1123 +++-- languages/messages/MessagesQqq.php | 1096 +++-- languages/messages/MessagesQu.php | 1116 +++-- languages/messages/MessagesRgn.php | 683 +++ languages/messages/MessagesRif.php | 114 +- languages/messages/MessagesRm.php | 358 +- languages/messages/MessagesRmy.php | 136 +- languages/messages/MessagesRo.php | 1111 +++-- languages/messages/MessagesRoa_rup.php | 22 +- languages/messages/MessagesRoa_tara.php | 940 +++-- languages/messages/MessagesRu.php | 1052 +++-- languages/messages/MessagesRue.php | 2069 +++++++++ languages/messages/MessagesRuq.php | 3 + languages/messages/MessagesRuq_cyrl.php | 10 +- languages/messages/MessagesRuq_grek.php | 9 - languages/messages/MessagesRuq_latn.php | 10 +- languages/messages/MessagesSa.php | 115 +- languages/messages/MessagesSah.php | 977 +++-- languages/messages/MessagesSc.php | 352 +- languages/messages/MessagesScn.php | 743 ++-- languages/messages/MessagesSco.php | 228 +- languages/messages/MessagesSd.php | 157 +- languages/messages/MessagesSdc.php | 323 +- languages/messages/MessagesSe.php | 195 +- languages/messages/MessagesSei.php | 197 +- languages/messages/MessagesSg.php | 6 +- languages/messages/MessagesSh.php | 584 ++- languages/messages/MessagesShi.php | 177 +- languages/messages/MessagesSi.php | 941 +++-- languages/messages/MessagesSimple.php | 3 + languages/messages/MessagesSk.php | 1034 +++-- languages/messages/MessagesSl.php | 1125 +++-- languages/messages/MessagesSli.php | 2856 +++++++++++++ languages/messages/MessagesSm.php | 6 +- languages/messages/MessagesSma.php | 115 +- languages/messages/MessagesSn.php | 16 +- languages/messages/MessagesSo.php | 86 +- languages/messages/MessagesSq.php | 552 +-- languages/messages/MessagesSr_ec.php | 1020 +++-- languages/messages/MessagesSr_el.php | 1014 +++-- languages/messages/MessagesSrn.php | 359 +- languages/messages/MessagesSs.php | 60 +- languages/messages/MessagesSt.php | 4 +- languages/messages/MessagesStq.php | 954 +++-- languages/messages/MessagesSu.php | 608 +-- languages/messages/MessagesSv.php | 993 +++-- languages/messages/MessagesSw.php | 792 ++-- languages/messages/MessagesSzl.php | 286 +- languages/messages/MessagesTa.php | 278 +- languages/messages/MessagesTcy.php | 85 +- languages/messages/MessagesTe.php | 1059 +++-- languages/messages/MessagesTet.php | 158 +- languages/messages/MessagesTg.php | 3 + languages/messages/MessagesTg_cyrl.php | 416 +- languages/messages/MessagesTg_latn.php | 2417 +++++++++++ languages/messages/MessagesTh.php | 910 ++-- languages/messages/MessagesTi.php | 21 +- languages/messages/MessagesTk.php | 972 +++-- languages/messages/MessagesTl.php | 999 +++-- languages/messages/MessagesTlh.php | 28 - languages/messages/MessagesTn.php | 8 +- languages/messages/MessagesTo.php | 113 +- languages/messages/MessagesTokipona.php | 11 +- languages/messages/MessagesTp.php | 3 + languages/messages/MessagesTpi.php | 16 +- languages/messages/MessagesTr.php | 1137 +++-- languages/messages/MessagesTs.php | 40 +- languages/messages/MessagesTt.php | 5 +- languages/messages/MessagesTt_cyrl.php | 500 ++- languages/messages/MessagesTt_latn.php | 222 +- languages/messages/MessagesTy.php | 23 +- languages/messages/MessagesTyv.php | 177 +- languages/messages/MessagesUdm.php | 17 +- languages/messages/MessagesUg.php | 3 + languages/messages/MessagesUg_arab.php | 3212 ++++++++++++++ languages/messages/MessagesUg_latn.php | 27 +- languages/messages/MessagesUk.php | 1074 +++-- languages/messages/MessagesUr.php | 522 ++- languages/messages/MessagesUz.php | 96 +- languages/messages/MessagesVe.php | 2 +- languages/messages/MessagesVec.php | 983 +++-- languages/messages/MessagesVep.php | 913 ++-- languages/messages/MessagesVi.php | 1002 +++-- languages/messages/MessagesVls.php | 34 +- languages/messages/MessagesVmf.php | 985 +++++ languages/messages/MessagesVo.php | 480 ++- languages/messages/MessagesVot.php | 642 +++ languages/messages/MessagesVro.php | 315 +- languages/messages/MessagesWa.php | 381 +- languages/messages/MessagesWar.php | 105 +- languages/messages/MessagesWo.php | 555 +-- languages/messages/MessagesWuu.php | 458 +- languages/messages/MessagesXal.php | 363 +- languages/messages/MessagesXh.php | 15 +- languages/messages/MessagesXmf.php | 61 +- languages/messages/MessagesYdd.php | 10 - languages/messages/MessagesYi.php | 880 ++-- languages/messages/MessagesYo.php | 623 +-- languages/messages/MessagesYue.php | 982 +++-- languages/messages/MessagesZa.php | 33 +- languages/messages/MessagesZea.php | 384 +- languages/messages/MessagesZh.php | 16 +- languages/messages/MessagesZh_classical.php | 3 + languages/messages/MessagesZh_cn.php | 4 +- languages/messages/MessagesZh_hans.php | 1011 +++-- languages/messages/MessagesZh_hant.php | 1068 +++-- languages/messages/MessagesZh_hk.php | 111 +- languages/messages/MessagesZh_min_nan.php | 3 + languages/messages/MessagesZh_mo.php | 3 + languages/messages/MessagesZh_my.php | 3 + languages/messages/MessagesZh_sg.php | 3 - languages/messages/MessagesZh_tw.php | 1748 ++------ languages/messages/MessagesZh_yue.php | 3 + languages/messages/MessagesZu.php | 33 +- maintenance/7zip.inc | 69 + maintenance/Doxyfile | 2 +- maintenance/FiveUpgrade.inc | 2 +- maintenance/Maintenance.php | 860 ++++ maintenance/README | 6 +- maintenance/addwiki.php | 412 +- maintenance/apache-ampersand.diff | 53 - maintenance/archives/patch-change_tag-indexes.sql | 21 + maintenance/archives/patch-eu_local_id.sql | 3 + maintenance/archives/patch-external_user.sql | 9 + .../archives/patch-filearchive-user-index.sql | 5 + .../archives/patch-filearhive-user-index.sql | 5 - maintenance/archives/patch-job.sql | 18 +- maintenance/archives/patch-l10n_cache.sql | 8 + .../archives/patch-log_search-rename-index.sql | 7 + maintenance/archives/patch-log_search.sql | 10 + maintenance/archives/patch-log_user_text.sql | 5 +- maintenance/archives/patch-mime_minor_length.sql | 10 + maintenance/archives/patch-rd_interwiki.sql | 6 + maintenance/archives/patch-tc-timestamp.sql | 4 + maintenance/archives/patch-transcache.sql | 2 +- maintenance/archives/patch-user_properties.sql | 22 + maintenance/archives/populateSha1.php | 59 - maintenance/archives/rebuildRecentchanges.inc | 123 - maintenance/archives/upgradeWatchlist.php | 67 - maintenance/attachLatest.php | 96 +- maintenance/attribute.php | 106 - maintenance/backup.inc | 18 +- maintenance/benchmarkPurge.php | 157 +- maintenance/changePassword.php | 78 +- maintenance/checkAutoLoader.php | 70 +- maintenance/checkBadRedirects.php | 75 +- maintenance/checkImages.php | 121 +- maintenance/checkSyntax.php | 296 ++ maintenance/checkUsernames.php | 44 +- maintenance/cleanupCaps.php | 57 +- maintenance/cleanupImages.php | 88 +- maintenance/cleanupSpam.php | 211 +- maintenance/cleanupTable.inc | 161 +- maintenance/cleanupTitles.php | 67 +- maintenance/cleanupWatchlist.php | 60 +- maintenance/clear_interwiki_cache.php | 56 +- maintenance/clear_stats.php | 71 +- maintenance/commandLine.inc | 269 +- maintenance/convertLinks.inc | 6 +- maintenance/convertLinks.php | 239 +- maintenance/convertUserOptions.php | 72 + maintenance/counter.php | 12 - maintenance/createAndPromote.php | 111 +- maintenance/deleteArchivedFiles.inc | 56 - maintenance/deleteArchivedFiles.php | 85 +- maintenance/deleteArchivedRevisions.inc | 34 - maintenance/deleteArchivedRevisions.php | 71 +- maintenance/deleteBatch.php | 172 +- maintenance/deleteDefaultMessages.php | 90 +- maintenance/deleteImageMemcached.php | 73 +- maintenance/deleteOldRevisions.inc | 68 - maintenance/deleteOldRevisions.php | 102 +- maintenance/deleteOrphanedRevisions.inc.php | 32 - maintenance/deleteOrphanedRevisions.php | 118 +- maintenance/deleteRevision.php | 104 +- maintenance/deleteSelfExternals.php | 54 + maintenance/doMaintenance.php | 102 + maintenance/dumpBackup.php | 20 +- maintenance/dumpInterwiki.inc | 3 +- maintenance/dumpInterwiki.php | 7 +- maintenance/dumpLinks.php | 60 +- maintenance/dumpSisterSites.php | 47 +- maintenance/dumpTextPass.php | 80 +- maintenance/dumpUploads.php | 87 +- maintenance/edit.php | 144 +- maintenance/fetchInterwiki.pl | 102 - maintenance/fetchText.php | 82 +- maintenance/findhooks.php | 285 +- maintenance/fixSlaveDesync.php | 335 +- maintenance/fixTimestamps.php | 200 +- maintenance/fixUserRegistration.php | 61 +- maintenance/fuzz-tester.php | 8 +- maintenance/gearman/gearman.inc | 2 +- maintenance/gearman/gearmanWorker.php | 2 + maintenance/generateSitemap.php | 158 +- maintenance/getLagTimes.php | 59 +- maintenance/getSlaveServer.php | 60 +- maintenance/getText.php | 58 + maintenance/httpSessionDownload.php | 57 + maintenance/ibm_db2/README | 40 +- maintenance/ibm_db2/tables.sql | 551 ++- maintenance/importDump.php | 13 +- maintenance/importImages.inc | 112 + maintenance/importImages.inc.php | 88 - maintenance/importImages.php | 168 +- maintenance/importLogs.inc | 144 - maintenance/importLogs.php | 27 - maintenance/importTextFile.php | 4 +- maintenance/importUseModWiki.php | 16 +- maintenance/initEditCount.php | 161 +- maintenance/initStats.inc | 57 - maintenance/initStats.php | 87 +- maintenance/install-utils.inc | 219 + maintenance/installExtension.php | 2 +- maintenance/interwiki.sql | 18 +- maintenance/lag.php | 51 + maintenance/language/StatOutputs.php | 14 +- maintenance/language/alltrans.php | 40 +- maintenance/language/checkDupeMessages.php | 118 + maintenance/language/checkExtensions.php | 4 +- maintenance/language/checkLanguage.inc | 39 +- maintenance/language/countMessages.php | 87 +- maintenance/language/date-formats.php | 102 +- maintenance/language/diffLanguage.php | 6 +- maintenance/language/digit2html.php | 68 +- maintenance/language/dumpMessages.php | 48 +- maintenance/language/generateNormalizerData.php | 137 + maintenance/language/lang2po.php | 225 +- maintenance/language/langmemusage.php | 58 +- maintenance/language/languages.inc | 18 + maintenance/language/makeMessageDB.php | 45 - maintenance/language/messageTypes.inc | 25 +- maintenance/language/messages.inc | 520 ++- maintenance/language/rebuildLanguage.php | 55 +- maintenance/language/transstat.php | 71 +- maintenance/language/writeMessagesArray.inc | 25 +- maintenance/mcc.php | 84 +- maintenance/mctest.php | 112 +- maintenance/mergeMessageFileList.php | 72 + maintenance/migrateUserGroup.php | 70 + maintenance/minify.php | 111 + maintenance/moveBatch.php | 157 +- maintenance/mwdocgen.php | 15 +- maintenance/namespace2sql.php | 18 - maintenance/namespaceDupes.php | 244 +- maintenance/nextJobDB.php | 127 +- maintenance/nukeNS.php | 158 +- maintenance/nukePage.inc | 91 - maintenance/nukePage.php | 114 +- maintenance/ora/patch_seq_names_pre1.16.sql | 8 + maintenance/ora/tables.sql | 1129 +++-- maintenance/ora/user.sql | 16 + maintenance/orphans.php | 371 +- maintenance/ourusers.php | 1 + maintenance/parserTests.inc | 731 +++- maintenance/parserTests.php | 15 +- maintenance/parserTests.txt | 972 +++-- maintenance/patchSql.php | 69 +- maintenance/populateCategory.php | 128 +- maintenance/populateLogSearch.inc | 80 + maintenance/populateLogSearch.php | 153 + maintenance/populateLogUsertext.php | 82 + maintenance/populateParentId.php | 119 +- maintenance/populateSha1.php | 101 + maintenance/postgres/archives/patch-l10n_cache.sql | 8 + maintenance/postgres/archives/patch-log_search.sql | 9 + .../postgres/archives/patch-update_sequences.sql | 20 + .../postgres/archives/patch-user_properties.sql | 8 + maintenance/postgres/compare_schemas.pl | 15 +- maintenance/postgres/mediawiki_mysql2postgres.pl | 6 +- maintenance/postgres/tables.sql | 71 +- maintenance/preprocessorFuzzTest.php | 5 +- maintenance/protect.php | 68 + maintenance/purgeList.php | 74 +- maintenance/purgeOldText.inc | 4 +- maintenance/purgeOldText.php | 46 +- maintenance/reassignEdits.inc.php | 143 - maintenance/reassignEdits.php | 197 +- maintenance/rebuildFileCache.php | 179 +- maintenance/rebuildImages.php | 8 +- maintenance/rebuildInterwiki.inc | 1 + maintenance/rebuildInterwiki.php | 3 +- maintenance/rebuildLocalisationCache.php | 133 + maintenance/rebuildall.php | 78 +- maintenance/rebuildmessages.php | 56 +- maintenance/rebuildrecentchanges.inc | 246 -- maintenance/rebuildrecentchanges.php | 292 +- maintenance/rebuildtextindex.inc | 66 - maintenance/rebuildtextindex.php | 132 +- maintenance/refreshImageCount.php | 65 +- maintenance/refreshLinks.inc | 202 - maintenance/refreshLinks.php | 313 +- maintenance/removeUnusedAccounts.inc | 46 - maintenance/removeUnusedAccounts.php | 150 +- maintenance/renameDbPrefix.php | 129 +- maintenance/renamewiki.php | 115 +- maintenance/renderDump.php | 75 +- maintenance/rollbackEdits.php | 97 + maintenance/runBatchedQuery.php | 60 + maintenance/runJobs.php | 141 +- maintenance/showJobs.php | 49 +- maintenance/showStats.php | 78 +- maintenance/sql.php | 108 +- maintenance/sqlite.php | 113 + maintenance/sqlite/archives/initial-indexes.sql | 39 +- .../sqlite/archives/patch-log_user_text.sql | 5 + maintenance/sqlite/archives/patch-rd_interwiki.sql | 5 + maintenance/sqlite/archives/patch-tc-timestamp.sql | 3 + maintenance/sqlite/archives/searchindex-fts3.sql | 18 + maintenance/sqlite/archives/searchindex-no-fts.sql | 25 + maintenance/stats.php | 127 +- maintenance/storage/compressOld.inc | 6 +- maintenance/storage/compressOld.php | 2 +- maintenance/storage/dumpRev.php | 111 +- maintenance/storage/fixBug20757.php | 314 ++ maintenance/storage/make-blobs | 11 +- maintenance/storage/moveToExternal.php | 2 +- maintenance/storage/orphanStats.php | 43 +- maintenance/storage/recompressTracked.php | 57 +- maintenance/storage/resolveStubs.php | 10 +- maintenance/storage/storageTypeStats.php | 98 + maintenance/storage/trackBlobs.php | 28 +- maintenance/tables.sql | 99 +- maintenance/testRunner.ora.sql | 37 + maintenance/tests/.svnignore | 6 + maintenance/tests/ApiSetup.php | 39 + maintenance/tests/ApiTest.php | 164 + maintenance/tests/CdbTest.php | 79 + maintenance/tests/DatabaseSqliteTest.php | 57 + maintenance/tests/DatabaseTest.php | 92 + maintenance/tests/GlobalTest.php | 212 + maintenance/tests/HttpTest.php | 567 +++ maintenance/tests/IPTest.php | 52 + maintenance/tests/ImageFunctionsTest.php | 48 + maintenance/tests/LanguageConverterTest.php | 148 + maintenance/tests/LicensesTest.php | 17 + maintenance/tests/LocalFileTest.php | 97 + maintenance/tests/Makefile | 23 + maintenance/tests/MediaWikiParserTest.php | 283 ++ maintenance/tests/MediaWiki_Setup.php | 28 + maintenance/tests/README | 24 + maintenance/tests/RevisionTest.php | 114 + maintenance/tests/SanitizerTest.php | 73 + maintenance/tests/SearchEngineTest.php | 138 + maintenance/tests/SearchMySQLTest.php | 26 + maintenance/tests/SearchUpdateTest.php | 103 + maintenance/tests/SiteConfigurationTest.php | 311 ++ maintenance/tests/TimeAdjustTest.php | 40 + maintenance/tests/TitleTest.php | 17 + maintenance/tests/XmlTest.php | 115 + maintenance/tests/bootstrap.php | 15 + maintenance/tests/phpunit.xml | 17 + maintenance/tests/test-prefetch-current.xml | 75 + maintenance/tests/test-prefetch-previous.xml | 57 + maintenance/tests/test-prefetch-stub.xml | 75 + maintenance/undelete.php | 51 +- maintenance/update.php | 56 +- maintenance/updateArticleCount.inc.php | 61 - maintenance/updateArticleCount.php | 113 +- maintenance/updateRestrictions.php | 160 +- maintenance/updateSearchIndex.inc | 115 - maintenance/updateSearchIndex.php | 186 +- maintenance/updateSpecialPages.php | 215 +- maintenance/updaters.inc | 532 ++- maintenance/upgrade1_5.php | 2 +- maintenance/userOptions.inc | 10 +- maintenance/users.sql | 6 +- maintenance/waitForSlave.php | 35 +- maintenance/wikipedia-interwiki.sql | 7 +- math/README | 4 +- math/render.ml | 6 +- math/texutil.ml | 4 +- math/texvc.ml | 6 +- php5.php5 | 2 - profileinfo.php | 132 +- redirect.phtml | 1 - serialized/Makefile | 16 +- serialized/README | 37 - serialized/normalize-ar.ser | 1 + serialized/normalize-ml.ser | 1 + serialized/serialize-localisation.php | 35 - skins/ArchLinux.php | 199 +- skins/Chick.php | 16 +- skins/CologneBlue.php | 274 +- skins/Modern.php | 107 +- skins/MonoBook.php | 154 +- skins/MySkin.php | 14 +- skins/Nostalgia.php | 29 +- skins/Simple.php | 31 +- skins/Skin.sample | 19 - skins/Standard.php | 182 +- skins/Vector.deps.php | 11 + skins/Vector.php | 767 ++++ skins/archlinux/IEMacFixes.css | 44 - skins/archlinux/discussionitem_icon.gif | Bin 949 -> 549 bytes skins/archlinux/file_icon.gif | Bin 921 -> 323 bytes skins/archlinux/link_icon.gif | Bin 942 -> 342 bytes skins/archlinux/lock_icon.gif | Bin 918 -> 918 bytes skins/archlinux/magnify-clip.png | Bin 237 -> 170 bytes skins/archlinux/mail_icon.gif | Bin 918 -> 321 bytes skins/archlinux/main.css | 318 +- skins/archlinux/rtl.css | 18 +- skins/archlinux/user.gif | Bin 923 -> 325 bytes skins/archlinux/video.png | Bin 215 -> 169 bytes skins/archlinux/wiki.png | Bin 23064 -> 22987 bytes skins/chick/main.css | 390 +- skins/common/IE80Fixes.css | 15 + skins/common/IEFixes.js | 205 +- skins/common/Makefile | 2 + skins/common/ajaxwatch.js | 53 +- skins/common/allmessages.js | 83 - skins/common/block.js | 37 +- skins/common/commonPrint.css | 36 +- skins/common/common_rtl.css | 7 +- skins/common/edit.js | 209 +- skins/common/history.js | 11 +- skins/common/htmlform.js | 40 + skins/common/images/Arr_.png | Bin 918 -> 246 bytes skins/common/images/Arr_r.xcf | Bin 1437 -> 0 bytes skins/common/images/Arr_u.png | Bin 1044 -> 425 bytes skins/common/images/Zoom_sans.gif | Bin 901 -> 901 bytes skins/common/images/add.png | Bin 0 -> 3329 bytes skins/common/images/ajax-loader.gif | Bin 0 -> 3208 bytes skins/common/images/arrow_first.svg | 85 - skins/common/images/arrow_left.svg | 78 - skins/common/images/be-tarask/button_bold.png | Bin 575 -> 554 bytes skins/common/images/be-tarask/button_italic.png | Bin 638 -> 592 bytes skins/common/images/be-tarask/button_link.png | Bin 550 -> 466 bytes skins/common/images/button_bold.png | Bin 978 -> 288 bytes skins/common/images/button_extlink.png | Bin 1093 -> 494 bytes skins/common/images/button_headline.png | Bin 497 -> 465 bytes skins/common/images/button_hr.png | Bin 372 -> 251 bytes skins/common/images/button_image.png | Bin 1110 -> 584 bytes skins/common/images/button_italic.png | Bin 975 -> 292 bytes skins/common/images/button_link.png | Bin 434 -> 337 bytes skins/common/images/button_math.png | Bin 730 -> 617 bytes skins/common/images/button_media.png | Bin 1155 -> 780 bytes skins/common/images/button_nowiki.png | Bin 375 -> 352 bytes skins/common/images/button_sig.png | Bin 1217 -> 953 bytes skins/common/images/button_template.png | Bin 362 -> 233 bytes skins/common/images/cyrl/button_italic.png | Bin 461 -> 460 bytes skins/common/images/cyrl/button_link.png | Bin 353 -> 347 bytes skins/common/images/de/button_bold.png | Bin 1013 -> 328 bytes skins/common/images/de/button_italic.png | Bin 1021 -> 351 bytes skins/common/images/fa/button_bold.png | Bin 504 -> 500 bytes skins/common/images/fa/button_headline.png | Bin 438 -> 434 bytes skins/common/images/fa/button_italic.png | Bin 577 -> 573 bytes skins/common/images/fa/button_link.png | Bin 538 -> 535 bytes skins/common/images/fileicon.xcf | Bin 26160 -> 0 bytes skins/common/images/gnu-fdl.png | Bin 1748 -> 1730 bytes skins/common/images/gnu-fdl.xcf | Bin 5578 -> 0 bytes skins/common/images/icons/fileicon-c.png | Bin 2995 -> 2211 bytes skins/common/images/icons/fileicon-cpp.png | Bin 2250 -> 1882 bytes skins/common/images/icons/fileicon-deb.png | Bin 5528 -> 4801 bytes skins/common/images/icons/fileicon-djvu.png | Bin 11137 -> 10752 bytes skins/common/images/icons/fileicon-dvi.png | Bin 13042 -> 12778 bytes skins/common/images/icons/fileicon-exe.png | Bin 5864 -> 5680 bytes skins/common/images/icons/fileicon-h.png | Bin 1195 -> 1191 bytes skins/common/images/icons/fileicon-html.png | Bin 7601 -> 7422 bytes skins/common/images/icons/fileicon-iso.png | Bin 6673 -> 6450 bytes skins/common/images/icons/fileicon-java.png | Bin 6825 -> 5989 bytes skins/common/images/icons/fileicon-mid.png | Bin 7191 -> 6657 bytes skins/common/images/icons/fileicon-mov.png | Bin 7946 -> 7716 bytes skins/common/images/icons/fileicon-o.png | Bin 2893 -> 2204 bytes skins/common/images/icons/fileicon-ogg.png | Bin 6143 -> 3750 bytes skins/common/images/icons/fileicon-pdf.png | Bin 5138 -> 4976 bytes skins/common/images/icons/fileicon-ps.png | Bin 3293 -> 3012 bytes skins/common/images/icons/fileicon-rm.png | Bin 4977 -> 2851 bytes skins/common/images/icons/fileicon-rpm.png | Bin 4753 -> 4103 bytes skins/common/images/icons/fileicon-svg.png | Bin 5193 -> 5094 bytes skins/common/images/icons/fileicon-tar.png | Bin 6544 -> 6347 bytes skins/common/images/icons/fileicon-tex.png | Bin 4203 -> 3997 bytes skins/common/images/icons/fileicon-ttf.png | Bin 3625 -> 3469 bytes skins/common/images/icons/fileicon-txt.png | Bin 6801 -> 3638 bytes skins/common/images/ksh/button_S_italic.png | Bin 3812 -> 3206 bytes skins/common/images/link_icon.gif | Bin 942 -> 342 bytes skins/common/images/magnify-clip.png | Bin 267 -> 204 bytes skins/common/images/mediawiki-small.xcf | Bin 36011 -> 0 bytes skins/common/images/mediawiki.png | Bin 23064 -> 22987 bytes skins/common/images/poweredby_mediawiki_88x31.png | Bin 1933 -> 1927 bytes skins/common/images/public-domain.png | Bin 2892 -> 2251 bytes skins/common/images/redirectltr.png | Bin 1024 -> 381 bytes skins/common/images/redirectrtl.png | Bin 1017 -> 381 bytes skins/common/images/remove.png | Bin 0 -> 3346 bytes skins/common/images/sort_down.gif | Bin 879 -> 464 bytes skins/common/images/sort_none.gif | Bin 877 -> 462 bytes skins/common/images/sort_up.gif | Bin 881 -> 466 bytes skins/common/images/spinner.gif | Bin 586 -> 4648 bytes skins/common/images/wiki.png | Bin 24954 -> 24801 bytes skins/common/jquery.js | 4384 ++++++++++++++++++++ skins/common/jquery.min.js | 433 ++ skins/common/metadata.js | 36 +- skins/common/mwsuggest.js | 1312 +++--- skins/common/oldshared.css | 61 +- skins/common/prefs.js | 205 +- skins/common/preview.js | 208 +- skins/common/protect.js | 2 +- skins/common/search.js | 50 + skins/common/shared.css | 587 ++- skins/common/sticky.js | 124 - skins/common/upload.js | 203 +- skins/common/wikibits.js | 782 ++-- skins/disabled/MonoBook.tpl | 200 - skins/disabled/MonoBookCBT.php | 1389 ------- skins/modern/discussionitem_icon.gif | Bin 949 -> 549 bytes skins/modern/file_icon.gif | Bin 921 -> 323 bytes skins/modern/link_icon.gif | Bin 942 -> 342 bytes skins/modern/lock_icon.gif | Bin 918 -> 321 bytes skins/modern/mail_icon.gif | Bin 918 -> 321 bytes skins/modern/main.css | 193 +- skins/modern/rtl.css | 6 +- skins/monobook/IEMacFixes.css | 44 - skins/monobook/discussionitem_icon.gif | Bin 949 -> 549 bytes skins/monobook/file_icon.gif | Bin 921 -> 323 bytes skins/monobook/link_icon.gif | Bin 942 -> 342 bytes skins/monobook/lock_icon.gif | Bin 918 -> 918 bytes skins/monobook/magnify-clip.png | Bin 237 -> 170 bytes skins/monobook/mail_icon.gif | Bin 918 -> 321 bytes skins/monobook/main.css | 318 +- skins/monobook/rtl.css | 18 +- skins/monobook/user.gif | Bin 923 -> 325 bytes skins/monobook/video.png | Bin 215 -> 169 bytes skins/monobook/wiki.png | Bin 23064 -> 22987 bytes skins/simple/discussionitem_icon.gif | Bin 949 -> 549 bytes skins/simple/file_icon.gif | Bin 921 -> 323 bytes skins/simple/link_icon.gif | Bin 942 -> 342 bytes skins/simple/lock_icon.gif | Bin 918 -> 321 bytes skins/simple/mail_icon.gif | Bin 918 -> 321 bytes skins/simple/main.css | 11 +- skins/simple/rtl.css | 6 +- skins/vector/Makefile | 18 + skins/vector/csshover.htc | 262 ++ skins/vector/cssjanus/COPYING | 13 + skins/vector/cssjanus/LICENSE | 202 + skins/vector/cssjanus/README | 91 + skins/vector/cssjanus/cssjanus.py | 574 +++ skins/vector/cssjanus/csslex.py | 114 + skins/vector/experiments/babaco-colors-a.css | 109 + skins/vector/experiments/babaco-colors-b.css | 67 + skins/vector/experiments/babaco-colors-c.css | 91 + skins/vector/experiments/images/new-border.png | Bin 0 -> 124 bytes .../experiments/images/new-portal-break-ltr.png | Bin 0 -> 891 bytes .../experiments/images/new-portal-break-rtl.png | Bin 0 -> 891 bytes skins/vector/experiments/images/page-base-fade.png | Bin 0 -> 306 bytes .../experiments/images/page-base-updated.png | Bin 0 -> 124 bytes .../vector/experiments/images/tab-active-first.png | Bin 0 -> 981 bytes .../vector/experiments/images/tab-active-last.png | Bin 0 -> 980 bytes skins/vector/experiments/images/tab-fade.png | Bin 0 -> 226 bytes skins/vector/experiments/images/tab-first.png | Bin 0 -> 1057 bytes skins/vector/experiments/images/tab-last.png | Bin 0 -> 1057 bytes skins/vector/experiments/images/tab-new-fade.png | Bin 0 -> 216 bytes skins/vector/experiments/new-tabs.css | 322 ++ skins/vector/images/arrow-down-icon.png | Bin 0 -> 181 bytes skins/vector/images/audio-icon.png | Bin 0 -> 345 bytes skins/vector/images/border.png | Bin 0 -> 119 bytes skins/vector/images/bullet-icon.png | Bin 0 -> 152 bytes skins/vector/images/document-icon.png | Bin 0 -> 345 bytes skins/vector/images/edit-icon.png | Bin 0 -> 358 bytes skins/vector/images/external-link-ltr-icon.png | Bin 0 -> 279 bytes skins/vector/images/external-link-rtl-icon.png | Bin 0 -> 277 bytes skins/vector/images/file-icon.png | Bin 0 -> 402 bytes skins/vector/images/link-icon.png | Bin 0 -> 429 bytes skins/vector/images/lock-icon.png | Bin 0 -> 370 bytes skins/vector/images/magnify-clip.png | Bin 0 -> 204 bytes skins/vector/images/mail-icon.png | Bin 0 -> 375 bytes skins/vector/images/news-icon.png | Bin 0 -> 359 bytes skins/vector/images/page-base.png | Bin 0 -> 119 bytes skins/vector/images/page-fade.png | Bin 0 -> 253 bytes skins/vector/images/portal-break-ltr.png | Bin 0 -> 287 bytes skins/vector/images/portal-break-rtl.png | Bin 0 -> 280 bytes skins/vector/images/portal-break.png | Bin 0 -> 242 bytes skins/vector/images/preferences-base.png | Bin 0 -> 119 bytes skins/vector/images/preferences-break.png | Bin 0 -> 286 bytes skins/vector/images/preferences-edge.png | Bin 0 -> 119 bytes skins/vector/images/preferences-fade.png | Bin 0 -> 248 bytes skins/vector/images/search-fade.png | Bin 0 -> 185 bytes skins/vector/images/search-ltr.png | Bin 0 -> 214 bytes skins/vector/images/search-rtl.png | Bin 0 -> 214 bytes skins/vector/images/tab-break.png | Bin 0 -> 263 bytes skins/vector/images/tab-current-fade.png | Bin 0 -> 121 bytes skins/vector/images/tab-normal-fade.png | Bin 0 -> 254 bytes skins/vector/images/talk-icon.png | Bin 0 -> 377 bytes skins/vector/images/user-icon.png | Bin 0 -> 345 bytes skins/vector/images/video-icon.png | Bin 0 -> 395 bytes skins/vector/images/watch-icon-loading.gif | Bin 0 -> 840 bytes skins/vector/images/watch-icons.png | Bin 0 -> 1745 bytes skins/vector/main-ltr.css | 1128 +++++ skins/vector/main-rtl.css | 1128 +++++ skins/vector/wiki-indexed.png | Bin 0 -> 8205 bytes skins/vector/wiki.png | Bin 0 -> 22987 bytes t/.htaccess | 1 - t/00-test.t | 10 - t/README | 52 - t/Search.inc | 161 - t/Test.php | 496 --- t/inc/Database.t | 53 - t/inc/Global.t | 154 - t/inc/IP.t | 60 - t/inc/ImageFunctions.t | 56 - t/inc/Language.t | 58 - t/inc/Licenses.t | 26 - t/inc/LocalFile.t | 77 - t/inc/Parser.t | 39 - t/inc/Revision.t | 79 - t/inc/Sanitizer.t | 64 - t/inc/Search.t | 14 - t/inc/Title.t | 32 - t/inc/Xml.t | 56 - t/maint/bom.t | 38 - t/maint/eol-style.t | 35 - t/maint/php-lint.t | 33 - t/maint/php-tag.t | 29 - t/maint/unix-newlines.t | 33 - tests/.htaccess | 1 - tests/.svnignore | 6 - tests/ArticleTest.php | 110 - tests/DatabaseTest.php | 80 - tests/GlobalTest.php | 212 - tests/ImageFunctionsTest.php | 48 - tests/LocalFileTest.php | 90 - tests/Makefile | 19 - tests/MediaWiki_TestCase.php | 51 - tests/README | 9 - tests/SearchEngineTest.php | 136 - tests/SearchMySQL4Test.php | 31 - tests/run-test.php | 7 - tests/test-prefetch-current.xml | 75 - tests/test-prefetch-previous.xml | 57 - tests/test-prefetch-stub.xml | 75 - wiki.phtml | 1 - 1299 files changed, 195056 insertions(+), 111529 deletions(-) delete mode 100644 AdminSettings.sample delete mode 100644 Makefile delete mode 100644 StartProfiler.php create mode 100644 StartProfiler.sample create mode 100644 cache/.htaccess create mode 100644 config/Installer.php create mode 100644 docs/distributors.txt create mode 100644 docs/export-0.4.xsd create mode 100644 docs/maintenance.txt create mode 100644 docs/php-memcached/README create mode 100644 includes/Cdb.php create mode 100644 includes/Cdb_PHP.php create mode 100644 includes/ConfEditor.php create mode 100644 includes/ExternalUser.php delete mode 100644 includes/FileStore.php create mode 100644 includes/HTMLForm.php create mode 100644 includes/HistoryPage.php create mode 100644 includes/Html.php create mode 100644 includes/JSMin.php create mode 100644 includes/LocalisationCache.php delete mode 100644 includes/PageHistory.php create mode 100644 includes/PoolCounter.php create mode 100644 includes/Preferences.php delete mode 100644 includes/SearchEngine.php delete mode 100644 includes/SearchIBM_DB2.php delete mode 100644 includes/SearchMySQL.php delete mode 100644 includes/SearchMySQL4.php delete mode 100644 includes/SearchOracle.php delete mode 100644 includes/SearchPostgres.php delete mode 100644 includes/SearchUpdate.php create mode 100644 includes/SquidPurgeClient.php create mode 100644 includes/WikiMap.php delete mode 100644 includes/api/ApiFormatJson_json.php create mode 100644 includes/api/ApiQueryTags.php create mode 100644 includes/api/ApiUpload.php create mode 100644 includes/api/ApiUserrights.php delete mode 100644 includes/cbt/CBTCompiler.php delete mode 100644 includes/cbt/CBTProcessor.php delete mode 100644 includes/cbt/README create mode 100644 includes/db/DatabaseMysql.php create mode 100644 includes/diff/DifferenceInterface.php delete mode 100644 includes/diff/HTMLDiff.php delete mode 100644 includes/diff/Nodes.php create mode 100644 includes/extauth/Hardcoded.php create mode 100644 includes/extauth/MediaWiki.php create mode 100644 includes/extauth/vB.php delete mode 100644 includes/filerepo/FileCache.php create mode 100644 includes/json/FormatJson.php create mode 100644 includes/json/Services_JSON.php create mode 100644 includes/media/GIF.php create mode 100644 includes/media/GIFMetadataExtractor.php create mode 100644 includes/parser/CoreTagHooks.php create mode 100644 includes/search/SearchEngine.php create mode 100644 includes/search/SearchIBM_DB2.php create mode 100644 includes/search/SearchMySQL.php create mode 100644 includes/search/SearchMySQL4.php create mode 100644 includes/search/SearchOracle.php create mode 100644 includes/search/SearchPostgres.php create mode 100644 includes/search/SearchSqlite.php create mode 100644 includes/search/SearchUpdate.php create mode 100644 includes/specials/SpecialActiveusers.php delete mode 100644 includes/specials/SpecialListUserRestrictions.php delete mode 100644 includes/specials/SpecialRestrictUser.php delete mode 100644 includes/specials/SpecialUploadMogile.php create mode 100644 includes/upload/UploadBase.php create mode 100644 includes/upload/UploadFromFile.php create mode 100644 includes/upload/UploadFromStash.php create mode 100644 includes/upload/UploadFromUrl.php delete mode 100644 install-utils.inc create mode 100644 languages/classes/LanguageAm.php create mode 100644 languages/classes/LanguageBh.php create mode 100644 languages/classes/LanguageGd.php create mode 100644 languages/classes/LanguageHi.php create mode 100644 languages/classes/LanguageLn.php create mode 100644 languages/classes/LanguageMg.php create mode 100644 languages/classes/LanguageMk.php create mode 100644 languages/classes/LanguageMl.php create mode 100644 languages/classes/LanguageMo.php create mode 100644 languages/classes/LanguageNso.php create mode 100644 languages/classes/LanguageRo.php create mode 100644 languages/classes/LanguageSe.php create mode 100644 languages/classes/LanguageSh.php create mode 100644 languages/classes/LanguageSma.php create mode 100644 languages/classes/LanguageTi.php create mode 100644 languages/classes/LanguageTl.php create mode 100644 languages/messages/MessagesCkb.php create mode 100644 languages/messages/MessagesCkb_arab.php create mode 100644 languages/messages/MessagesCkb_latn.php create mode 100644 languages/messages/MessagesCps.php create mode 100644 languages/messages/MessagesFrr.php create mode 100644 languages/messages/MessagesHa.php create mode 100644 languages/messages/MessagesKiu.php create mode 100644 languages/messages/MessagesKo_kp.php create mode 100644 languages/messages/MessagesKoi.php create mode 100644 languages/messages/MessagesKrc.php delete mode 100644 languages/messages/MessagesLld.php create mode 100644 languages/messages/MessagesLtg.php create mode 100644 languages/messages/MessagesMrj.php create mode 100644 languages/messages/MessagesPcd.php create mode 100644 languages/messages/MessagesPrg.php create mode 100644 languages/messages/MessagesRgn.php create mode 100644 languages/messages/MessagesRue.php delete mode 100644 languages/messages/MessagesRuq_grek.php create mode 100644 languages/messages/MessagesSli.php create mode 100644 languages/messages/MessagesTg_latn.php delete mode 100644 languages/messages/MessagesTlh.php create mode 100644 languages/messages/MessagesUg_arab.php create mode 100644 languages/messages/MessagesVmf.php create mode 100644 languages/messages/MessagesVot.php delete mode 100644 languages/messages/MessagesYdd.php create mode 100644 maintenance/7zip.inc create mode 100644 maintenance/Maintenance.php delete mode 100644 maintenance/apache-ampersand.diff create mode 100644 maintenance/archives/patch-change_tag-indexes.sql create mode 100644 maintenance/archives/patch-eu_local_id.sql create mode 100644 maintenance/archives/patch-external_user.sql create mode 100644 maintenance/archives/patch-filearchive-user-index.sql delete mode 100644 maintenance/archives/patch-filearhive-user-index.sql create mode 100644 maintenance/archives/patch-l10n_cache.sql create mode 100644 maintenance/archives/patch-log_search-rename-index.sql create mode 100644 maintenance/archives/patch-log_search.sql create mode 100644 maintenance/archives/patch-mime_minor_length.sql create mode 100644 maintenance/archives/patch-rd_interwiki.sql create mode 100644 maintenance/archives/patch-tc-timestamp.sql create mode 100644 maintenance/archives/patch-user_properties.sql delete mode 100644 maintenance/archives/populateSha1.php delete mode 100644 maintenance/archives/rebuildRecentchanges.inc delete mode 100644 maintenance/archives/upgradeWatchlist.php delete mode 100644 maintenance/attribute.php create mode 100644 maintenance/checkSyntax.php create mode 100644 maintenance/convertUserOptions.php delete mode 100644 maintenance/counter.php delete mode 100644 maintenance/deleteArchivedFiles.inc delete mode 100644 maintenance/deleteArchivedRevisions.inc delete mode 100644 maintenance/deleteOldRevisions.inc delete mode 100644 maintenance/deleteOrphanedRevisions.inc.php create mode 100644 maintenance/deleteSelfExternals.php create mode 100644 maintenance/doMaintenance.php delete mode 100644 maintenance/fetchInterwiki.pl create mode 100644 maintenance/getText.php create mode 100644 maintenance/httpSessionDownload.php create mode 100644 maintenance/importImages.inc delete mode 100644 maintenance/importImages.inc.php delete mode 100644 maintenance/importLogs.inc delete mode 100644 maintenance/importLogs.php delete mode 100644 maintenance/initStats.inc create mode 100644 maintenance/install-utils.inc create mode 100644 maintenance/lag.php create mode 100644 maintenance/language/checkDupeMessages.php create mode 100644 maintenance/language/generateNormalizerData.php delete mode 100644 maintenance/language/makeMessageDB.php create mode 100644 maintenance/mergeMessageFileList.php create mode 100644 maintenance/migrateUserGroup.php create mode 100644 maintenance/minify.php delete mode 100644 maintenance/namespace2sql.php delete mode 100644 maintenance/nukePage.inc create mode 100644 maintenance/ora/patch_seq_names_pre1.16.sql create mode 100644 maintenance/ora/user.sql create mode 100644 maintenance/populateLogSearch.inc create mode 100644 maintenance/populateLogSearch.php create mode 100644 maintenance/populateLogUsertext.php create mode 100644 maintenance/populateSha1.php create mode 100644 maintenance/postgres/archives/patch-l10n_cache.sql create mode 100644 maintenance/postgres/archives/patch-log_search.sql create mode 100644 maintenance/postgres/archives/patch-update_sequences.sql create mode 100644 maintenance/postgres/archives/patch-user_properties.sql create mode 100644 maintenance/protect.php delete mode 100644 maintenance/reassignEdits.inc.php create mode 100644 maintenance/rebuildLocalisationCache.php delete mode 100644 maintenance/rebuildrecentchanges.inc delete mode 100644 maintenance/rebuildtextindex.inc delete mode 100644 maintenance/refreshLinks.inc delete mode 100644 maintenance/removeUnusedAccounts.inc create mode 100644 maintenance/rollbackEdits.php create mode 100644 maintenance/runBatchedQuery.php create mode 100644 maintenance/sqlite.php create mode 100644 maintenance/sqlite/archives/patch-log_user_text.sql create mode 100644 maintenance/sqlite/archives/patch-rd_interwiki.sql create mode 100644 maintenance/sqlite/archives/patch-tc-timestamp.sql create mode 100644 maintenance/sqlite/archives/searchindex-fts3.sql create mode 100644 maintenance/sqlite/archives/searchindex-no-fts.sql create mode 100644 maintenance/storage/fixBug20757.php create mode 100644 maintenance/storage/storageTypeStats.php create mode 100644 maintenance/testRunner.ora.sql create mode 100644 maintenance/tests/.svnignore create mode 100644 maintenance/tests/ApiSetup.php create mode 100644 maintenance/tests/ApiTest.php create mode 100644 maintenance/tests/CdbTest.php create mode 100644 maintenance/tests/DatabaseSqliteTest.php create mode 100644 maintenance/tests/DatabaseTest.php create mode 100644 maintenance/tests/GlobalTest.php create mode 100644 maintenance/tests/HttpTest.php create mode 100644 maintenance/tests/IPTest.php create mode 100644 maintenance/tests/ImageFunctionsTest.php create mode 100644 maintenance/tests/LanguageConverterTest.php create mode 100644 maintenance/tests/LicensesTest.php create mode 100644 maintenance/tests/LocalFileTest.php create mode 100644 maintenance/tests/Makefile create mode 100644 maintenance/tests/MediaWikiParserTest.php create mode 100644 maintenance/tests/MediaWiki_Setup.php create mode 100644 maintenance/tests/README create mode 100644 maintenance/tests/RevisionTest.php create mode 100644 maintenance/tests/SanitizerTest.php create mode 100644 maintenance/tests/SearchEngineTest.php create mode 100644 maintenance/tests/SearchMySQLTest.php create mode 100644 maintenance/tests/SearchUpdateTest.php create mode 100644 maintenance/tests/SiteConfigurationTest.php create mode 100644 maintenance/tests/TimeAdjustTest.php create mode 100644 maintenance/tests/TitleTest.php create mode 100644 maintenance/tests/XmlTest.php create mode 100644 maintenance/tests/bootstrap.php create mode 100644 maintenance/tests/phpunit.xml create mode 100644 maintenance/tests/test-prefetch-current.xml create mode 100644 maintenance/tests/test-prefetch-previous.xml create mode 100644 maintenance/tests/test-prefetch-stub.xml delete mode 100644 maintenance/updateArticleCount.inc.php delete mode 100644 maintenance/updateSearchIndex.inc delete mode 100644 serialized/README create mode 100644 serialized/normalize-ar.ser create mode 100644 serialized/normalize-ml.ser delete mode 100644 serialized/serialize-localisation.php delete mode 100644 skins/Skin.sample create mode 100644 skins/Vector.deps.php create mode 100644 skins/Vector.php delete mode 100644 skins/archlinux/IEMacFixes.css create mode 100644 skins/common/IE80Fixes.css create mode 100644 skins/common/Makefile delete mode 100644 skins/common/allmessages.js create mode 100644 skins/common/htmlform.js delete mode 100644 skins/common/images/Arr_r.xcf create mode 100644 skins/common/images/add.png create mode 100644 skins/common/images/ajax-loader.gif delete mode 100644 skins/common/images/arrow_first.svg delete mode 100644 skins/common/images/arrow_left.svg delete mode 100644 skins/common/images/fileicon.xcf delete mode 100644 skins/common/images/gnu-fdl.xcf delete mode 100644 skins/common/images/mediawiki-small.xcf create mode 100644 skins/common/images/remove.png create mode 100644 skins/common/jquery.js create mode 100644 skins/common/jquery.min.js create mode 100644 skins/common/search.js delete mode 100644 skins/common/sticky.js delete mode 100644 skins/disabled/MonoBook.tpl delete mode 100644 skins/disabled/MonoBookCBT.php delete mode 100644 skins/monobook/IEMacFixes.css create mode 100644 skins/vector/Makefile create mode 100644 skins/vector/csshover.htc create mode 100644 skins/vector/cssjanus/COPYING create mode 100644 skins/vector/cssjanus/LICENSE create mode 100644 skins/vector/cssjanus/README create mode 100644 skins/vector/cssjanus/cssjanus.py create mode 100644 skins/vector/cssjanus/csslex.py create mode 100644 skins/vector/experiments/babaco-colors-a.css create mode 100644 skins/vector/experiments/babaco-colors-b.css create mode 100644 skins/vector/experiments/babaco-colors-c.css create mode 100644 skins/vector/experiments/images/new-border.png create mode 100644 skins/vector/experiments/images/new-portal-break-ltr.png create mode 100644 skins/vector/experiments/images/new-portal-break-rtl.png create mode 100644 skins/vector/experiments/images/page-base-fade.png create mode 100644 skins/vector/experiments/images/page-base-updated.png create mode 100644 skins/vector/experiments/images/tab-active-first.png create mode 100644 skins/vector/experiments/images/tab-active-last.png create mode 100644 skins/vector/experiments/images/tab-fade.png create mode 100644 skins/vector/experiments/images/tab-first.png create mode 100644 skins/vector/experiments/images/tab-last.png create mode 100644 skins/vector/experiments/images/tab-new-fade.png create mode 100644 skins/vector/experiments/new-tabs.css create mode 100644 skins/vector/images/arrow-down-icon.png create mode 100644 skins/vector/images/audio-icon.png create mode 100644 skins/vector/images/border.png create mode 100644 skins/vector/images/bullet-icon.png create mode 100644 skins/vector/images/document-icon.png create mode 100644 skins/vector/images/edit-icon.png create mode 100644 skins/vector/images/external-link-ltr-icon.png create mode 100644 skins/vector/images/external-link-rtl-icon.png create mode 100644 skins/vector/images/file-icon.png create mode 100644 skins/vector/images/link-icon.png create mode 100644 skins/vector/images/lock-icon.png create mode 100644 skins/vector/images/magnify-clip.png create mode 100644 skins/vector/images/mail-icon.png create mode 100644 skins/vector/images/news-icon.png create mode 100644 skins/vector/images/page-base.png create mode 100644 skins/vector/images/page-fade.png create mode 100644 skins/vector/images/portal-break-ltr.png create mode 100644 skins/vector/images/portal-break-rtl.png create mode 100644 skins/vector/images/portal-break.png create mode 100644 skins/vector/images/preferences-base.png create mode 100644 skins/vector/images/preferences-break.png create mode 100644 skins/vector/images/preferences-edge.png create mode 100644 skins/vector/images/preferences-fade.png create mode 100644 skins/vector/images/search-fade.png create mode 100644 skins/vector/images/search-ltr.png create mode 100644 skins/vector/images/search-rtl.png create mode 100644 skins/vector/images/tab-break.png create mode 100644 skins/vector/images/tab-current-fade.png create mode 100644 skins/vector/images/tab-normal-fade.png create mode 100644 skins/vector/images/talk-icon.png create mode 100644 skins/vector/images/user-icon.png create mode 100644 skins/vector/images/video-icon.png create mode 100644 skins/vector/images/watch-icon-loading.gif create mode 100644 skins/vector/images/watch-icons.png create mode 100644 skins/vector/main-ltr.css create mode 100644 skins/vector/main-rtl.css create mode 100644 skins/vector/wiki-indexed.png create mode 100644 skins/vector/wiki.png delete mode 100644 t/.htaccess delete mode 100644 t/00-test.t delete mode 100644 t/README delete mode 100644 t/Search.inc delete mode 100644 t/Test.php delete mode 100644 t/inc/Database.t delete mode 100644 t/inc/Global.t delete mode 100644 t/inc/IP.t delete mode 100644 t/inc/ImageFunctions.t delete mode 100644 t/inc/Language.t delete mode 100644 t/inc/Licenses.t delete mode 100644 t/inc/LocalFile.t delete mode 100644 t/inc/Parser.t delete mode 100644 t/inc/Revision.t delete mode 100644 t/inc/Sanitizer.t delete mode 100644 t/inc/Search.t delete mode 100644 t/inc/Title.t delete mode 100644 t/inc/Xml.t delete mode 100644 t/maint/bom.t delete mode 100644 t/maint/eol-style.t delete mode 100644 t/maint/php-lint.t delete mode 100644 t/maint/php-tag.t delete mode 100644 t/maint/unix-newlines.t delete mode 100644 tests/.htaccess delete mode 100644 tests/.svnignore delete mode 100644 tests/ArticleTest.php delete mode 100644 tests/DatabaseTest.php delete mode 100644 tests/GlobalTest.php delete mode 100644 tests/ImageFunctionsTest.php delete mode 100644 tests/LocalFileTest.php delete mode 100644 tests/Makefile delete mode 100644 tests/MediaWiki_TestCase.php delete mode 100644 tests/README delete mode 100644 tests/SearchEngineTest.php delete mode 100644 tests/SearchMySQL4Test.php delete mode 100644 tests/run-test.php delete mode 100644 tests/test-prefetch-current.xml delete mode 100644 tests/test-prefetch-previous.xml delete mode 100644 tests/test-prefetch-stub.xml diff --git a/.gitignore b/.gitignore index 9e90d89e..a94ae0af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /images/ +/cache/ *~ /LocalSettings.php /AdminSettings.php diff --git a/AdminSettings.sample b/AdminSettings.sample deleted file mode 100644 index 8b6fe993..00000000 --- a/AdminSettings.sample +++ /dev/null @@ -1,32 +0,0 @@ - tag which + was displayed to the user +* (bug 21150) SQLite no longer raise an error when deleting files +* (bug 20880) Fixed updater failure on SQLite backend +* upgrade1_5.php now requires to be run --update option to prevent confusion +* Fixed a CSS validation issue which allowed external images to be included + into wikis where that is disallowed by configuration. +* Fixed a data leakage vulnerability for private wikis using img_auth.php or + similar image access authentication schemes. Check user permissions before + streaming out scaled images from thumb.php. + +=== Changes since 1.15.0 === + +* Fixed fatal errors for unusual file repository configurations, such as + ForeignAPIRepo. +* Fixed the "change password" link on Special:Preferences to have the correct + returnto parameter. +* (bug 19693) Fixed cross-site scripting vulnerability in Special:Block + +=== Changes since 1.15.0rc1 === + +* Removed category redirect feature, implementation was incomplete. +* (bug 18846) Remove update_password_format(), unnecessary, destroys all + passwords if a wiki with $wgPasswordSalt=false is upgraded with the web + installer. +* (bug 19127) Documentation warning for PostgreSQL users who run update.php: + use the same user in AdminSettings.php as in LocalSettings.php. +* Fixed possible web invocation of some maintenance scripts, due to the use of + include() instead of require(). A full exploit would require a very strange + web server configuration. +* Localisation updates. + +=== Configuration changes in 1.15 === + +* Added $wgNewPasswordExpiry, to specify an expiry time (in seconds) to + temporary passwords +* Added $wgUseTwoButtonsSearchForm to choose the Search form behavior/look +* Added $wgNoFollowDomainExceptions to allow exempting particular domain names + from rel="nofollow" on external links +* (bug 12970) Brought back $wgUseImageResize. +* Added $wgRedirectOnLogin to allow specifying a specifc page to redirect users + to upon logging in (ex: "Main Page") +* Add $wgExportFromNamespaces for enabling/disabling the "export all from + namespace" option (disabled by default) + +=== New features in 1.15 === + +* (bug 2242) Add an expiry time to temporary passwords +* (bug 9947) Add PROTECTIONLEVEL parser function to return the protection level + for the current page for a given action +* (bug 17002) Add &minor= and &summary= as parameters in the url when editing, + to automatically add a summary or a minor edit. +* (bug 16852) padleft and padright now accept multiletter pad characters +* When using 'UserCreateForm' hook to add new checkboxes into + Special:UserLogin/signup, the messages can now contain HTML to allow + hyperlinking to the site's Terms of Service page, for example +* Add new hook 'UserLoadFromDatabase' that is called while loading a user + from the database. +* (bug 17045) Options on the block form are prefilled with the options of the + existing block when modifying an existing block. +* (bug 17055) "(show/hide)" links to Special:RevisionDelete now use a CSS class + rather than hardcoded HTML tags +* Added new hook 'WantedPages::getSQL' into SpecialWantedpages.php to allow + extensions to alter the SQL query which is used to get the list of wanted + pages +* (bugs 16957/16969) Add show/hide to preferences for RC patrol options on + specialpages +* (bug 11443) Auto-noindex user/user talk pages for blocked user +* (bug 11644) Add $wgMaxRedirects variable to control how many redirects are + recursed through until the "destination" page is reached. +* Add $wgInvalidRedirectTargets variable to prevent redirects to certain + special pages. +* Use HTML5 rel attributes for some links, where appropriate +* Added optional alternative Search form look - Go button & Advanced search + link instead of Go button & Search button +* (bug 2314) Add links to user custom CSS and JS to Special:Preferences +* More helpful error message on raw page access if PHP_SELF isn't set +* (bug 13040) Gender switch in user preferences +* (bug 13040) {{GENDER:}} magic word for interface messages +* (bug 3301) Optionally sort user list according to account creation time +* Remote description pages for foreign file repos are now fetched in the + content language. +* (bug 17180) If $wgUseFileCache is enabled, $wgShowIPinHeader is automatically + set to false. +* (bug 16604) Mark non-patrolled edits in feeds with "!" +* (bug 16604) Show title/rev in IRC for patrol log +* (bug 16854) Whether a page is being parsed as a preview or section preview + can now be determined and set with ParserOptions. +* Wrap message 'confirmemail_pending' into a div with CSS classes "error" and + "mw-confirmemail-pending" +* (bug 8249) The magic words for namespaces and pagenames can now be used as + parser functions to return the desired namespace or normalized title/title + part for a given title. +* (bug 17110) Styled #mw-data-after-content in cologneblue.css to match the + rest of the font +* (bug 7556) Time zone names in signatures lack i18n +* (bug 3311) Automatic category redirects +* (bug 17236) Suppress 'watch user page link' for IP range blocks +* Wrap message 'searchresulttext' (Special:Search) into a div with + class "mw-searchresult" +* (bug 15283) Interwiki imports can now fetch included templates +* Treat svn:// URLs as external links by default +* New function to convert namespace text for display (only applies on wiki with + LanguageConverter class) +* (bug 17379) Contributions-title is now parsed for magic words. +* Preprocessor output now cached in memcached. +* (bug 14468) Lines in classic RecentChanges and Watchlist have classes + "mw-line-odd" and "mw-line-even" to make styling using css possible. +* (bug 17311) Add a note beside the gender selection menu to tell users that + this information will be public +* Localize time zone regions in Special:Preferences +* Add NUMBEROFACTIVEUSERS magic word, which is like NUMBEROFUSERS, but uses + the active users data from site_stats. +* Add a tag on redirected page views +* Replace hardcoded '...' as indication of a truncation with the + 'ellipsis' message +* Wrap warning message 'editinginterface' into a div with class + 'mw-editinginterface' +* (bug 17497) Oasis opendocument added to mime.types +* Remove the link to Special:FileDuplicateSearch from the "file history" section + of image description pages as the list of duplicated files is shown in the + next section anyway. +* Added $wgRateLimitsExcludedIPs, to allow specific IPs to be whitelisted from + rate limits. +* (bug 14981) Shared repositories can now have display names, located at + Mediawiki:Shared-repo-name-REPONAME, where REPONAME is the name in + $wgForeignFileRepos +* Special:ListUsers: Sort list of usergroups by alphabet +* (bug 16762) Special:Movepage now shows a list of subpages when possible +* (bug 17585) Hide legend on Special:Specialpages from non-privileged users +* Added $wgUseTagFilter to control enabling of filter-by-change-tag +* (bug 17291) MediaWiki:Nocontribs now has an optional $1 parameter for the + username +* Wrap special page summary message '$specialPageName-summary' into a div + with class 'mw-specialpage-summary' +* $wgSummarySpamRegex added to handle edit summary spam. This is used *instead* + of $wgSpamRegex for edit summary checks. Text checks still use $wgSpamRegex. +* New function to convert content text to specified language (only applies on wiki with + LanguageConverter class) +* (bug 17844) Redirect users to a specific page when they log in, see + $wgRedirectOnLogin +* Added a link to Special:UserRights on Special:Contributions for privileged users +* (bug 10336) Added new magic word {{REVISIONUSER}}, which displays the editor + of the displayed revision's author user name +* LinkerMakeExternalLink now has an $attribs parameter for link attributes and + a $linkType parameter for the type of external link being made +* (bug 17785) Dynamic dates surrounded with a tag, fixing sortable tables with + dynamic dates. +* (bug 4582) Provide preference-based autoformatting of unlinked dates with the dateformat + parser function. +* (bug 17886) Special:Export now allows you to export a whole namespace (limited to 5000 pages) +* (bug 17714) Limited TIFF upload support now built in if 'tif' extension is + enabled. Image width and height are now recognized, and when using ImageMagick, + optional flattening to PNG or JPEG for inline display can be enabled by setting + $wgTiffThumbnailType +* Renamed two input IDs on Special:Log from 'page' and 'user' to 'mw-log-page' and + 'mw-log-user', respectively +* Added $wgInvalidUsernameCharacters to disallow certain characters in + usernames during registration (such as "@") +* Added $wgUserrightsInterwikiDelimiter to allow changing the delimiter + used in Special:UserRights to denote the user should be searched for + on a different database +* Add a class if 'missingsummary' is triggered to allow styling of the summary + line +* Title attributes are now always blank on framed and thumbnailed images, and default to blank + on inline images instead of defaulting to the image's filename. Additionally, the alt + attribute now defaults to the filename on framed and thumbnailed images if no caption or alt + attribute is specified. + +=== Bug fixes in 1.15 === +* (bug 16968) Special:Upload no longer throws useless warnings. +* (bug 17000) Special:RevisionDelete now checks if the database is locked + before trying to delete the edit. +* (bug 16852) padleft and padright now handle multibyte characters correctly +* (bug 17010) maintenance/namespaceDupes.php now add the suffix recursively if + the destination page exists +* (bug 17035) Special:Upload now fails gracefully if PHP's file_uploads has + been disabled +* Fixing the caching issue by using -{T|xxx}- syntax (only applies on wiki with + LanguageConverter class) +* Improving the efficiency by using -{A|xxx}- syntax (only applies on wiki with + LanguageConverter class) +* (bug 17054) Added more descriptive errors in Special:RevisionDelete +* (bug 11527) Diff on page with one revision shows "Next" link to same diff +* (bug 8065) Fix summary forcing for new pages +* (bug 10569) redirects to Special:Mypage and Special:Mytalk are no longer + allowed by default. Change $wgInvalidRedirectTargets to re-enable. +* (bug 3043) Feed links of given page are now preceded by standard feed icon +* (bug 17150) escapeLike now escapes literal \ properly +* Inconsistent use of sysop, admin, administrator in system messages changed + to 'administrator' +* (bug 14423) Check block flag validity for block logging +* DB transaction and slave-lag avoidance tweaks for Email Notifications +* (bug 17104) Removed [Mark as patrolled] link for already patrolled revisions +* (bug 17106) Added 'redirect=no' and 'mw-redirect' class to redirects at + "user contributions" +* Rollback links on new pages removed from "user contributions" +* (bug 15811) Re-upload form tweaks: license fields removed, destination locked, + comment label uses better message +* Whole HTML validation ($wgValidateAllHtml) now works with external tidy +* Parser tests no longer fail when $wgExternalLinkTarget is set in + LocalSettings +* (bug 15391) catch DBQueryErrors on external storage insertion. This avoids + error messages on save were the edit in fact is saved. +* (bug 17184) Remove duplicate "z" accesskey in MonoBook +* Parser tests no longer fail when $wgAlwaysUseTidy is set in LocalSettings.php +* Removed redundant dupe warnings on reupload for the same title. Dupe warnings + for identical files at different titles are still given. +* Add 'change tagging' facility, where changes can be tagged internally with + certain designations, which are displayed on various summaries of changes, + and the entries can be styled with CSS. +* (bug 17207) Fix regression breaking category page display on PHP 5.1 +* Categoryfinder utility class no longer fails on invalid input or gives wrong + results for category names that include pseudo-namespaces +* (bug 17252) Galician numbering format +* (bug 17146) Fix for UTF-8 and short word search for some possible MySQL + configs +* (bug 7480) Internationalize database error message +* (bug 16555) Number of links to mediawiki.org scaled back on post-installation +* (bug 14938) Removing a section no longer leaves excess whitespace +* (bug 17304) Fixed fatal error when thumbnails couldn't be generated for file + history +* (bug 17283) Remove double URL escaping in show/hide links for log entries + and RevisionDeleteForm::__construct +* (bug 17105) Numeric table sorting broken +* (bug 17231) Transcluding special pages on wikis using language conversion no + longer affects the page title +* (bug 6702) Default system messages updated/improved +* (bug 17190) User ID on preference page no longer has delimeters +* (bug 17341) "Powered by MediaWiki" should be on the left on RTL wikis +* (bug 17404) "userrights-interwiki" right was missing in User::$mCoreRights +* (bug 7509) Separation strings should be configurable * (bug 17420) Send the correct content type from action=raw when the HTML file cache is enabled. +* (bug 12746) Do not allow new password e-mails when wiki is in read-only mode +* (bug 17478) Fixed a PHP Strict standards error in + maintenance/cleanupWatchlist.php +* (bug 17488) RSS/Atom links in left toolbar are now localized in classic skin +* (bug 17472) use print << parameters in Special:Contributions feeds (RSS and Atom) + now point to the actual contributors' feed. +* ForeignApiRepos now fetch MIME types, rather than trying to figure it locally +* Special:Import: Do not show input field for import depth if + $wgExportMaxLinkDepth == 0 +* (bug 17570) $wgMaxRedirects is now correctly respected when following + redirects (was previously one more than $wgMaxRedirects) +* (bug 16335) __NONEWSECTIONLINK__ magic word to suppress new section link. +* (bug 17581) Wrong index name in PostgreSQL's updater: was rc_timestamp_nobot, + changed to rc_timestamp_bot * (bug 17437) Fixed incorrect link to web-based installer -* (bug 17527) Fixed missing MySQL-specific options in installer +* (bug 17538) Use shorter URLs in elements +* (bug 13778) Hidden input added to the search form so that using the Enter key + on IE will do a fulltext search like clicking the button does +* (bug 1061) CSS-added icons next to links display through the text and makes + it unreadable in RTL +* Special:Wantedtemplates now works on PostgreSQL +* (bug 14414) maintenance/updateSpecialPages.php no longer throws error with + PostgreSQL +* (bug 17546) Correct Tongan language native name is "lea faka-Tonga" +* (bug 17621) Special:WantedFiles has no link to Special:Whatlinkshere +* (bug 17460) Client ecoding is now correctly set for PostgreSQL +* (bug 17648) Prevent floats from intruding into edit area in previews if no + toolbar present +* (bug 17692) Added (list of members) link to 'user' in Special:Listgrouprights +* (bug 17707) Show file destination as plain text if &wpForReUpload=1 +* (bug 10172) Moved setting of "changed since last visit" flags out of the job + queue +* (bug 17761) "show/hide" link in page history in now works for the first + displayed revision if it's not the current one +* (bug 17722) Fix regression where users are unable to change temporary passwords +* (bug 17799) Special:Random no longer throws a database error when a non- + namespace is given, silently falls back to NS_MAIN +* (bug 17751) The message for bad titles in WantedPages is now localized +* (bug 17860) Moving a page in the "MediaWiki" namespace using SuppressRedirect + no longer corrupts the message cache +* (bug 17900) Fixed User Groups interface log display after saving groups. +* (bug 17897) Fixed string offset error in
 tags
+* (bug 17778) MediaWiki:Catseparator can now have HTML entities
+* (bug 17676) Error on Special:ListFiles when using Postgres
+* Special:Export doesn't use raw SQL queries anymore
+* (bug 14771) Thumbnail links to individual DjVu pages have two no longer have
+  two "page" parameters
+* (bug 17972) Special:FileDuplicateSearch form now works correctly on wikis that
+  don't use PathInfo or short urls
+* (bug 17990) trackback.php now has a trackback.php5 alias and works with 
+  $wgScriptExtension
+* (bug 14990) Parser tests works again with PostgreSQL
+* (bug 11487) Special:Protectedpages doesn't list protections with pr_expiry
+  IS NULL
+* (bug 18018) Deleting a file redirect leaves behind a malfunctioning redirect
+* (bug 17537) Disable bad zlib.output_compression output on HTTP 304 responses
+* (bug 11213) [edit] section links in printable version no longer appear when you cut-and-paste article text
+* (bug 17405) "Did you mean" to mirror Go/Search behavior of original request
+* (bug 18116) 'edittools' is now output identically on edit and upload pages
+* (bug 17241) The diffonly URI parameter should cascade to "Next edit" and "Previous edit" diff links
+* (bug 16823) 'Sidebar search form should not use Special:Search view URL as target'
+* (bug 16343) Non-existing, but in use, category pages can be "go" match hits
+* Fixed a CSS validation issue which allowed external images to be included
+  into wikis where that is disallowed by configuration.
+* Fixed a data leakage vulnerability for private wikis using img_auth.php or
+  similar image access authentication schemes. Check user permissions before
+  streaming out scaled images from thumb.php.
+
+== API changes in 1.15 ==
+* (bug 16858) Revamped list=deletedrevs to make listing deleted contributions
+  and listing all deleted pages possible
+* (bug 16844) Added clcategories parameter to prop=categories
+* (bug 17025) Add "fileextension" parameter to meta=siteinfo&siprop=
+* (bug 17048) Show the 'new' flag in list=usercontribs for the revision that
+  created the page, even if it's not the top revision
+* (bug 17069) Added ucshow=patrolled|!patrolled to list=usercontribs
+* action=delete respects $wgDeleteRevisionsLimit and the bigdelete user right
+* (bug 15949) Add undo functionality to action=edit
+* (bug 16483) Kill filesort in ApiQueryBacklinks caused by missing parentheses.
+  Building query properly now using makeList()
+* (bug 17182) Fix pretty printer so URLs with parentheses in them are
+  autolinked correctly
+* (bug 17224) Added siprop=rightsinfo to meta=siteinfo
+* (bug 17239) Added prop=displaytitle to action=parse
+* (bug 17317) Added watch parameter to action=protect
+* (bug 17007) Added export and exportnowrap parameters to action=query
+* (bug 17326) BREAKING CHANGE: Changed output format for iiprop=metadata
+* (bug 17355) Added auwitheditsonly parameter to list=allusers
+* (bug 17007) Added action=import
+* BREAKING CHANGE: Removed rctitles parameter from list=recentchanges because
+  of performance concerns
+* Listing (semi-)deleted revisions and log entries as well in prop=revisions
+  and list=logevents
+* (bug 11430) BREAKING CHANGE: Modules may return fewer results than the
+  limit and still set a query-continue in some cases
+* (bug 17357) Added movesubpages parameter to action=move
+* (bug 17433) Added bot flag to list=watchlist&wlprop=flags output
+* (bug 16740) Added list=protectedtitles
+* Added mainmodule and pagesetmodule parameters to action=paraminfo
+* (bug 17502) meta=siteinfo&siprop=namespacealiases no longer lists namespace
+  aliases already listed in siprop=namespaces
+* (bug 17529) rvend ignored when rvstartid is specified
+* (bug 17626) Added uiprop=email to list=userinfo
+* (bug 13209) Added rvdiffto parameter to prop=revisions
+* Manual language conversion improve: Now we can include both ";" and ":" in
+  conversion rules
+* (bug 17795) Don't report views count on meta=siteinfo if $wgDisableCounters 
+  is set
+* (bug 17774) Don't hide read-restricted modules like action=query from users
+  without read rights, but throw an error when they try to use them.
+* Don't hide write modules when $wgEnableWriteAPI is false, but throw an error
+  when someone tries to use them
+* BREAKING CHANGE: action=purge requires write rights and, for anonymous users,
+  a POST request
+* (bug 18099) Using appendtext to edit a non-existent page causes an interface
+  message to be included in the page text
+* Fixed the circular template inclusion check, was broken when the loop 
+  involved redirects. Without this, infinite recursion within the parser is
+  possible.
+* (bug 18601) generator=backlinks returns invalid continue parameter
+* (bug 18597) Internal error with empty generator= parameter
+* (bug 18617) Add xml:space="preserve" attribute to relevant tags in XML output
+* (bug 17611) Provide a sensible error message on install when the SQLite data
+  directory is wrong.
+
+=== Languages updated in 1.15 ===
+
+MediaWiki supports over 300 languages. Many localisations are updated
+regularly. Below only new and removed languages are listed, as well as
+changes to languages because of MediaZilla reports.
+
+* Austrian German (de-at) (new)
+* Swiss Standard German (de-ch) (new)
+* Simplified Gan Chinese (gan-hans) (new)
+* Traditional Gan Chinese (gan-hant) (new)
+* Literary Chinese (lzh) (new)
+* Uyghur (Latin script) (ug-latn) (renamed from 'ug')
+* Veps (vep) (new)
+* Võro (vro) (renamed from fiu-vro)
+* (bug 17151) Add magic word alias for #redirect for Vietnamese
+* (bug 17288) Messages improved for default language (English)
+* (bug 12937) Update native name for Afar
+* (bug 16909) 'histlegend' now reuses messages instead of copying them
+* (bug 17832) action=delete returns 'unknownerror' instead of 'permissiondenied' when
+  the user is blocked
+* Traditional/Simplified Gan Chinese conversion support
+
+== MediaWiki 1.14 ==
 
 === Configuration changes in 1.14 ===
 
@@ -5408,7 +5798,7 @@ User accounts:
   groups. Note that this does *not* allow you to make pages which are only
   accessible to certain groups.
   
-  For details see: http://meta.wikimedia.org/wiki/Help:User_rights
+  For details see: http://www.mediawiki.org/wiki/Manual:User_rights
 
 E-mail:
   User-to-user e-mail can now be restricted to require a mail-back confirmation
@@ -5658,8 +6048,8 @@ Various bugfixes, small features, and a few experimental things:
 
 * 'live preview' reduces preview reload burden on supported browsers
 * support for external editors for files and wiki pages:
-  http://meta.wikimedia.org/wiki/Help:External_editors
-* Schema reworking: http://meta.wikimedia.org/wiki/Proposed_Database_Schema_Changes/October_2004
+  http://www.mediawiki.org/wiki/Manual:External_editors
+* Schema reworking: http://www.mediawiki.org/wiki/Proposed_Database_Schema_Changes/October_2004
 * (bug 15) Allow editors to view diff of their change before actually submitting an edit
 * (bug 190) Hide your own edits on the watchlist
 * (bug 510): Special:Randompage now works for other namespaces than NS_MAIN.
@@ -6342,7 +6732,7 @@ release for relevant bug fixes; see the changelog later in this file.
 If you have trouble, remember to read this whole file and the online FAQ page
 before asking for help:
 
-http://meta.wikimedia.org/wiki/MediaWiki_FAQ
+http://www.mediawiki.org/wiki/Manual:FAQ
 
 
 === READ THIS FIRST: Upgrading ===
diff --git a/Makefile b/Makefile
deleted file mode 100644
index b414ffa3..00000000
--- a/Makefile
+++ /dev/null
@@ -1,28 +0,0 @@
-#
-# This Makefile is used to test some MediaWiki functions. If you
-# want to install MediaWiki, point your browser to ./config/
-#
-
-# Configuration:
-PROVE_BIN="prove"
-
-# Describe our tests:
-BASE_TEST=$(wildcard t/*.t)
-INCLUDES_TESTS=$(wildcard t/inc/*t)
-MAINTENANCE_TESTS=$(wildcard t/maint/*t)
-
-# Build groups:
-FAST_TESTS=$(BASE_TEST) $(INCLUDES_TESTS)
-ALL_TESTS=$(BASE_TEST) $(INCLUDES_TESTS) $(MAINTENANCE_TESTS)
-
-test: t/Test.php
-	$(PROVE_BIN) $(ALL_TESTS)
-
-fast: t/Test.php
-	$(PROVE_BIN) $(FAST_TESTS)
-
-maint:
-	$(PROVE_BIN) $(MAINTENANCE_TESTS)
-
-verbose: t/Test.php
-	$(PROVE_BIN) -v $(ALL_TESTS) | egrep -v '^ok'
diff --git a/RELEASE-NOTES b/RELEASE-NOTES
index 4e5effb2..d3983380 100644
--- a/RELEASE-NOTES
+++ b/RELEASE-NOTES
@@ -1,461 +1,995 @@
 = MediaWiki release notes =
 
-Security reminder: MediaWiki does not require PHP's register_globals
-setting since version 1.2.0. If you have it on, turn it *off* if you can.
-
-== MediaWiki 1.15.5 ==
+== MediaWiki 1.16.0 ==
 
 2010-07-28
 
-This is a security and maintenance release.
+This is a stable release of the MediaWiki 1.16 branch.
+
+=== Summary of selected changes in 1.16 ===
+
+Selected changes since MediaWiki 1.15 that may be of interest:
+
+* Watchlists now have RSS/Atom feeds. RSS feeds generally are now hidden, 
+  since Atom is a better protocol and is supported by virtually all clients.
+
+* It's now possible to block users from sending email via Special:Emailuser.
+
+* The maintenance script system was overhauled. Most maintenance scripts now 
+  have a useful help page when you run them with --help.
 
-MediaWiki is now using a "continuous integration" development model with
-quarterly snapshot releases. The latest development code is always kept
-"ready to run", and in fact runs our own sites on Wikipedia.
+* AdminSettings.php is no longer required in order to run maintenance scripts. 
+  You can just set $wgDBadminuser and $wgDBadminpassword in your 
+  LocalSettings.php instead.
 
-Release branches will continue to receive security updates for about a year
-from first release, but nonessential bugfixes and feature developments
-will be made on the development trunk and appear in the next quarterly release.
+* The preferences system was overhauled. Preferences are stored in a more 
+  compact format. Changes to site default preferences will automatically 
+  affect all users who have not chosen a different preference.
 
-Those wishing to use the latest code instead of a branch release can obtain
-it from source control: http://www.mediawiki.org/wiki/Download_from_SVN
+* Support for SQLite was improved. Some broken features were fixed, and it 
+  now has an efficient full-text search.
 
-== Changes since 1.15.4 ==
+* The user groups ACL system was improved by allowing rights to be revoked, 
+  instead of just granted.
 
+* A new localisation caching system was introduced, which will make MediaWiki 
+  faster for almost everyone, especially when lots of extensions are enabled.
+
+By default, this new system makes a lot of database queries. If your database 
+is particularly slow, or if your system administrator limits your query count, 
+or if you want to squeeze as much performance as possible out of Mediawiki, 
+set $wgCacheDirectory to a writable path on the local filesystem. Make sure 
+you have the DBA extension for PHP installed, this will improve performance 
+further.
+
+== Changes since 1.16 beta 3 ==
+
+* (bug 23769) Disabled HTML 5 client-side form validation. Was introduced in 
+  1.16 beta 1, but is currently poorly supported by browsers.
+* (bug 23175) Re-added window.ta variable for backwards compatibility.
+* (bug 23264) Fixed breakage of various command line scripts due to extra line
+  endings being inserted by Maintenance::output().
+* Fixed HTTP client functionality with safe_mode=On.
+* Fixed parser tests broken in 1.16 beta 3.
+* For Oracle DB backend: fixed parser tests and table prefix feature.
+* (bug 23767) Fixed PHP warning when REQUEST_URI is blank (IIS issue).
+* Fixed plural function for Northern Sami (se)
+* (bug 23597) Fixed conflicts between ID attributes in the Vector skin and 
+  parser-generated heading IDs. Renamed head, panel, head-base and page-base.
+* Disabled $wgHitcounterUpdateFreq>1 feature on SQLite, does not work yet.
+* (bug 23465) Don't ignore the predefined destination filename on 
+  Special:Upload after following a red link to a file.
+* In SQLite full-text search feature: fixed "move page" feature, was non-
+  functional.
 * (bug 24565) Fixed Cache-Control headers sent from API modules, to protect
   user privacy in the case where an attacker can access the wiki through the
   same HTTP proxy as a logged-in user.
-* Fixed a minor cookie header parsing issue causing incorrect Cache-Control 
-  headers to be sent.
 * Fixed an XSS vulnerability in profileinfo.php for installations with 
   $wgEnableProfileInfo = true (false by default)
-* For backwards compatibility with extensions from 1.14.x or before, restored 
-  the original function ApiMain::requestWriteMode(). 
-* In API login "need token" responses, added the cookieprefix and sessionid 
-  fields, as in MediaWiki 1.16.x. This is an improvement to the CSRF fix 
-  introduced in 1.15.3.
+* Fixed a case where an X-Vary-Options header was sent despite $wgUseXVO being
+  false. Fixed a minor header parsing issue when $wgUseXVO = true.
+* Fixed a register_globals arbitrary inclusion vulnerability in 
+  MediaWikiParserTest.php, introduced in 1.16 beta 1.
 
-== Changes since 1.15.3 ==
+== Changes since 1.16 beta 2 ==
 
+* Fixed bugs in the [[Special:Userlogin]] and [[Special:Emailuser]] handling of
+  invalid usernames.
+* Fixed sorting in [[Special:Allmessages]]
+* (bug 23113) Fixed title in the show/hide links on diff pages
+* (bug 23117) Fixed API rollback, was returning "badtoken" for valid requests
+* (bug 23127) Re-added missing $1 parameter to the uploadtext message
+* Fixed a bug in the Vector skin where personal tools display behind the logo
+* (bug 23139) Fixed a bug in edit conflict resolution, where both textboxes 
+  showed the same text.
+* (bug 23115, bug 23124) Fixed various problems with  and <h1> elements
+  in page views and previews when the language converter is enabled.
+* (bug 23148) Fixed a local path disclosure vulnerability in ImageMagick image
+  scaling, which was introduced in 1.16 beta 1. 
+* Improved error checking on installer.
+* (bug 22970) Fixed a JavaScript error in the upload destination conflict 
+  check.
+* (bug 23167) Check the watch checkbox by default if the watchcreations 
+  preference is set.
+* (bug 23171) Improve IE6 version check to avoid false positives.
+* (bug 23176) Fixed upload warning override feature "upload new version", 
+  broken in 1.16 beta 1.
+* Fixed regression in unwatch links sent out in notification emails. When the 
+  mailing job was deferred via the job queue, the title was incorrect.
 * (bug 23534) Fixed SQL query error in API list=allusers.
+* Fixed a bug in uploads for non-JavaScript clients. An empty string was used
+  as the default destination filename, instead of the source filename as 
+  expected.
 * (bug 23371) Fixed CSRF vulnerability in "e-mail me my password", "create 
   account" and "create by e-mail" features of [[Special:Userlogin]]
 * (bug 23687) Fixed XSS vulnerability affecting IE clients only, due to a CSS 
   validation issue.
+* Fixed a DoS vulnerability in ImageMagick image scaling. ImageMagick 
+  expanded wildcard characters "?" and "*" in image filenames, potentially 
+  causing large numbers of images to be scaled in response to a single request.
+  The fix for this involves breaking the scaling of such image filenames until
+  ImageMagick 6.6.1-5 or later is deployed, see bug 23361 for more details.
+* (bug 23608) Fixed invalid HTML in diff pages.
 
-=== Changes since 1.15.2 ===
+=== Changes since 1.16 beta 1 ===
 
-* (bug 22828) Fixed deletion on SQLite.
+* Fixed errors in maintenance/patchSql.php
+* (bug 19627) Fix regression from r57867 where HTMLForm would output 
+  <element classes="foo bar"> rather than <element class="foo bar">
+* Fixed broken "-r" option to maintenance/lag.php
 * (bug 23076) Fixed login CSRF vulnerability. Logins now require a token to 
   be submitted along with the user name and password.
 
-=== Changes since 1.15.1 ===
+=== Configuration changes in 1.16 ===
 
-* The installer now includes a check for a data corruption issue with certain
-  versions of libxml2 2.7 and PHP earlier than 5.2.9, and also for a PHP bug 
-  present in the official release of PHP 5.3.1.
-* (bug 20239) MediaWiki:Imagemaxsize does not contain anymore a <br /> tag which
-  was displayed to the user
-* (bug 21150) SQLite no longer raise an error when deleting files
-* (bug 20880) Fixed updater failure on SQLite backend
-* upgrade1_5.php now requires to be run --update option to prevent confusion
-* Fixed a CSS validation issue which allowed external images to be included 
-  into wikis where that is disallowed by configuration.
-* Fixed a data leakage vulnerability for private wikis using img_auth.php or 
-  similar image access authentication schemes. Check user permissions before 
-  streaming out scaled images from thumb.php.
-
-=== Changes since 1.15.0 ===
-
-* Fixed fatal errors for unusual file repository configurations, such as 
-  ForeignAPIRepo.
-* Fixed the "change password" link on Special:Preferences to have the correct
-  returnto parameter.
-* (bug 19693) Fixed cross-site scripting vulnerability in Special:Block
-
-=== Changes since 1.15.0rc1 ===
-
-* Removed category redirect feature, implementation was incomplete.
-* (bug 18846) Remove update_password_format(), unnecessary, destroys all 
-  passwords if a wiki with $wgPasswordSalt=false is upgraded with the web 
-  installer.
-* (bug 19127) Documentation warning for PostgreSQL users who run update.php: 
-  use the same user in AdminSettings.php as in LocalSettings.php. 
-* Fixed possible web invocation of some maintenance scripts, due to the use of
-  include() instead of require(). A full exploit would require a very strange
-  web server configuration.
-* Localisation updates.
-
-=== Configuration changes in 1.15 ===
-
-* Added $wgNewPasswordExpiry, to specify an expiry time (in seconds) to
-  temporary passwords
-* Added $wgUseTwoButtonsSearchForm to choose the Search form behavior/look
-* Added $wgNoFollowDomainExceptions to allow exempting particular domain names
-  from rel="nofollow" on external links
-* (bug 12970) Brought back $wgUseImageResize.
-* Added $wgRedirectOnLogin to allow specifying a specifc page to redirect users
-  to upon logging in (ex: "Main Page")
-* Add $wgExportFromNamespaces for enabling/disabling the "export all from 
-  namespace" option (disabled by default)
-
-=== New features in 1.15 ===
-
-* (bug 2242) Add an expiry time to temporary passwords
-* (bug 9947) Add PROTECTIONLEVEL parser function to return the protection level
-  for the current page for a given action
-* (bug 17002) Add &minor= and &summary= as parameters in the url when editing,
-  to automatically add a summary or a minor edit.
-* (bug 16852) padleft and padright now accept multiletter pad characters
-* When using 'UserCreateForm' hook to add new checkboxes into
-  Special:UserLogin/signup, the messages can now contain HTML to allow
-  hyperlinking to the site's Terms of Service page, for example
-* Add new hook 'UserLoadFromDatabase' that is called while loading a user
-  from the database.
-* (bug 17045) Options on the block form are prefilled with the options of the
-  existing block when modifying an existing block.
-* (bug 17055) "(show/hide)" links to Special:RevisionDelete now use a CSS class
-  rather than hardcoded HTML tags
-* Added new hook 'WantedPages::getSQL' into SpecialWantedpages.php to allow
-  extensions to alter the SQL query which is used to get the list of wanted
-  pages
-* (bugs 16957/16969) Add show/hide to preferences for RC patrol options on
-  specialpages
-* (bug 11443) Auto-noindex user/user talk pages for blocked user
-* (bug 11644) Add $wgMaxRedirects variable to control how many redirects are
-  recursed through until the "destination" page is reached.
-* Add $wgInvalidRedirectTargets variable to prevent redirects to certain
-  special pages.
-* Use HTML5 rel attributes for some links, where appropriate
-* Added optional alternative Search form look - Go button & Advanced search
-  link instead of Go button & Search button
-* (bug 2314) Add links to user custom CSS and JS to Special:Preferences
-* More helpful error message on raw page access if PHP_SELF isn't set
-* (bug 13040) Gender switch in user preferences
-* (bug 13040) {{GENDER:}} magic word for interface messages
-* (bug 3301) Optionally sort user list according to account creation time
-* Remote description pages for foreign file repos are now fetched in the
-  content language.
-* (bug 17180) If $wgUseFileCache is enabled, $wgShowIPinHeader is automatically
-  set to false.
-* (bug 16604) Mark non-patrolled edits in feeds with "!"
-* (bug 16604) Show title/rev in IRC for patrol log
-* (bug 16854) Whether a page is being parsed as a preview or section preview
-  can now be determined and set with ParserOptions.
-* Wrap message 'confirmemail_pending' into a div with CSS classes "error" and
-  "mw-confirmemail-pending"
-* (bug 8249) The magic words for namespaces and pagenames can now be used as
-  parser functions to return the desired namespace or normalized title/title
-  part for a given title.
-* (bug 17110) Styled #mw-data-after-content in cologneblue.css to match the
-  rest of the font
-* (bug 7556) Time zone names in signatures lack i18n
-* (bug 3311) Automatic category redirects
-* (bug 17236) Suppress 'watch user page link' for IP range blocks
-* Wrap message 'searchresulttext' (Special:Search) into a div with
-  class "mw-searchresult"
-* (bug 15283) Interwiki imports can now fetch included templates
-* Treat svn:// URLs as external links by default
-* New function to convert namespace text for display (only applies on wiki with
-  LanguageConverter class)
-* (bug 17379) Contributions-title is now parsed for magic words.
-* Preprocessor output now cached in memcached.
-* (bug 14468) Lines in classic RecentChanges and Watchlist have classes
-  "mw-line-odd" and "mw-line-even" to make styling using css possible.
-* (bug 17311) Add a note beside the gender selection menu to tell users that
-  this information will be public
-* Localize time zone regions in Special:Preferences
-* Add NUMBEROFACTIVEUSERS magic word, which is like NUMBEROFUSERS, but uses
-  the active users data from site_stats.
-* Add a <link rel="canonical"> tag on redirected page views
-* Replace hardcoded '...' as indication of a truncation with the
-  'ellipsis' message
-* Wrap warning message 'editinginterface' into a div with class
-  'mw-editinginterface'
-* (bug 17497) Oasis opendocument added to mime.types
-* Remove the link to Special:FileDuplicateSearch from the "file history" section
-  of image description pages as the list of duplicated files is shown in the 
-  next section anyway.
+* (bug 18222) $wgMinimalPasswordLength default is now 1
+* $wgSessionHandler can be used to configure session.save_handler
+* $wgLocalFileRepo/$wgForeignFileRepos now have a 'fileMode' parameter to
+  be used when uploading/moving files
+* (bug 18761) $wgHiddenPrefs is a new array for specifying preferences not
+  to be shown to users
+* $wgAllowRealName and $wgAllowUserSkin were deprecated in favor of
+  $wgHiddenPrefs[] = 'realname', but the former are still retained
+  for backwards-compatibility
+* (bug 9257) $wgRCMaxAge now defaults to three months
+* $wgDevelopmentWarnings can be set to true to show warnings about deprecated
+  functions and other potential errors when developing.
+* Subpages are now enabled in the MediaWiki namespace by default.  This is
+  mainly a cosmetic change, and does not in any way affect the MessageCache,
+  which was already effectively treating the namespace as if it had subpages.
+* Oracle: maintenance/ora/user.sql script for creating DB user on oracle with
+  appropriate privileges. Creating this user with web-install page requires
+  oci8.privileged_connect set to On in php.ini.
+* Removed UserrightsChangeableGroups hook introduced in 1.14
+* Added $wgCacheDirectory, to replace $wgFileCacheDirectory,
+  $wgLocalMessageCache, and any other local caches which need a place to put
+  files.
+* $wgFileCacheDirectory is no longer set to anything by default, and so either
+  needs to be set explicitly, or $wgCacheDirectory needs to be set instead.
+* $wgLocalMessageCache has been removed. Instead, set $wgUseLocalMessageCache
+  to true
+* Removed $wgEnableSerializedMessages and $wgCheckSerialized. Similar
+  functionality is now available via $wgLocalisationCacheConf.
+* $wgMessageCache->addMessages() is deprecated. Messages added via this
+  interface will not appear in Special:AllMessages.
+* $wgRegisterInternalExternals can be used to record external links pointing
+  to same server
+* (bug 19907) $wgCrossSiteAJAXdomains and $wgCrossSiteAJAXdomainExceptions added
+  to control which external domains may access the API via cross-site AJAX.
+* $wgMaintenanceScripts for extensions to add their scripts to the default list
+* $wgMemoryLimit has been added, default value '50M'
+* $wgExtraRandompageSQL is deprecated, the SpecialRandomGetRandomTitle hook
+  should be used instead
+* (bug 20489) $wgIllegalFileChars added to override the default list of illegal
+  characters in file names.
+* (bug 19646) $wgImgAuthDetails added  to display reason access to uploaded file
+  was denied to users(img_auth only)
+* (bug 19646) $wgImgAuthPublicTest added to test to see if img_auth set up
+  correctly (img_auth only)
+* $wgUploadMaintenance added to disable file deletions and restorations during
+  maintenance
+* $wgCapitalLinkOverrides added to configure per-namespace capitalization
+* (bug 21172) $wgSorbsUrl can now be an array with multiple DNSBL and renamed
+  to $wgDnsBlacklistUrls (backward compatibility kept)
+* $wgEnableHtmlDiff has been removed
+* (bug 3340) $wgBlockCIDRLimit added (default: 16) to configure the low end of
+  CIDR ranges for blocking
+* $wgUseInstantCommons added for quick and easy enabling of Commons as a remote
+  file repository
+* $wgDBAhandler added to choose a DBA handler when using CACHE_DBA
+* $wgPreviewOnOpenNamespaces for extensions that create namespaces that behave
+  similarly to the category namespace.
+* $wgEnableSorbs renamed to $wgDnsBlacklistUrls ($wgEnableSorbs kept for
+  backward compatibility)
+* $wgUploadNavigationUrl now also affects images inline images that do not
+  exist. In that case the URL will get (?|&)wpDestFile=<filename> appended to
+  it as appropriate.
+* If $wgLocaltimezone is null, use the server's timezone as the default for
+  signatures. This was always the behaviour documented in DefaultSettings.php
+  but has not been the actual behaviour for some time: instead, UTC was used
+  by default.
+* Added $wgExtensionAssetsPath, to decouple assets serving from $wgScriptPath.
+  If not specified it will default to $wgScriptPath/extensions
+* Added $wgCountTotalSearchHits to make search UI display total number of hits
+  with some search engines.
+* Added $wgAdvertisedFeedTypes to decide what feed types (RSS, Atom, both, or
+  neither) MediaWiki advertises.  Default is array( 'atom' ), so RSS is no
+  longer advertised by default (but it still works).
+* Added $wgMemCachedTimeout, controls how long to wait for data from the
+  memcached servers.
+* New configuration variables $wgDebugTimestamps and $wgDebugPrintHttpHeaders
+  for controlling debug output.
+* New $wgBlockDisablesLogin when set to true disallows blocked users from
+  logging in.
+* (bug 8790) Metadata edition ($wgUseMetadataEdit) has been moved to a separate
+  extension "MetadataEdit".
+
+=== New features in 1.16 ===
+
+* Add CSS defintion of the 'wikitable' class to shared.css
+* (bug 17163) Added MediaWiki:Talkpageheader which will be displayed when
+  viewing talk pages
+* Superfluous border="0" removed from images
+* Added new hook 'MessageCacheReplace' into MessageCache.php. For instance
+  to allow extensions to update caches in similar way as MediaWiki invalidates
+  a cached MonoBook sidebar
+* Special:AllPages: Move hardcoded styles from code to CSS
+* (bug 18529) New hook: SoftwareInfo for adding information about the software
+  to Special:Version
+* Added $wgExtPGAlteredFields to allow extensions to easily alter the data
+  type of columns when using the Postgres backend.
+* (bug 16950) Show move log when viewing/creating a deleted page
+* (bug 18242) Show the Subversion revision number per extensions in
+  Special:Version
+* (bug 18420) Missing file revisions are handled gracefully now
+* (bug 9219) Auth plugins can control editing RealName/Email/Nick preferences
+* (bug 18466) Add note or warning when overruling a move (semi-)protection
+* (bug 18342) insertTags works in edit summary box
+* (bug 18411) The upload form also checks post_max_size
+* Watchlist now has a specialized <div> tag that contains a unique class for
+  each page
+* Added Minguo calendar support for the Taiwan Chinese language
+* Database: unionQueries function to be used for UNION sql construction, so
+  it can be overloaded on DB abstraction level for DB specific functionality
+* (bug 18849) Implement Japanese and North Korean calendars
+* (bug 5755) Introduce {{CURRENTMONTH1}} and {{LOCALMONTH1}} to display the
+  month number without the leading zero
+* (bug 13456) categoriespagetext supports PLURAL
+* (bug 18860) Blocks of IPs affecting registered users can now block email
+* (bug 17093) Date and time are separate parameters in Special:BlockList
+* (bug 11484) Added ISO speed rating to default collapsed EXIF metadata view
+* (bug 14866) Messages 'recentchangeslinked-toolbox' and
+  'recentchangeslinked-toolbox' were added to allow more fine grained
+  customisation of the user interface
+* DISPLAYTITLE now accepts a limited amount of wiki markup (the single-quote
+  items)
+* Special:Search now could search terms in all variant-forms. ONLY apply on
+  wikis enabled LanguageConverter.
+* Add autopromote condition APCOND_BLOCKED to autopromote blocked users to
+  various user groups.
+* Add $wgRevokePermissions as a means of restricting a group's rights. The
+  syntax is identical to $wgGroupPermissions, but users in these groups will
+  have these rights stripped from them.
+* Added a PHP port of CDB (constant database), for improved local caching when
+  the DBA extension is not available.
+* Introduced a new system for localisation caching. The system is based around
+  fast fetches of individual messages, minimising memory overhead and startup
+  time in the typical case. The database backend will be used by default, but
+  set $wgCacheDirectory to get a faster CDB-based implementation.
+* Expanded the number of variables which can be set in the extension messages
+  files.
+* Added a feature to allow per-article process pool size control for the parsing
+  task, to limit resource usage when the cache for a heavily-viewed article is
+  invalidated. Requires an external daemon.
+* (bug 19576) Moved the id attribues from the anchors accompanying section
+  headers to the <span class="mw-headline"> elements within the section headers,
+  removing the redundant anchor elements.
+* Parser::setFunctionTagHook now can be used to add a new tag which is parsed at
+  preprocesor level.
+* Added $wgShowArchiveThumbnails, allowing sysadmins to disable thumbnail
+  display for old versions of images.
+* In watchlists and Special:RecentChanges, the difference in page size now
+  appears in dark green if bytes were added and dark red if bytes were removed.
+* Added FSRepo configuration properties thumbUrl and thumbDir, to allow the
+  thumbnails to be stored in a separate location to the source images.
+* If config/ directory is not executable, the command to make it executable
+  now asks the user to cd to the correct directory
+* Add experimental new external authentication framework, ExternalAuth
+* (bug 18768) Remove AdminSettings requirements. Maintenance environment
+  will still load it if it exists, but it's not required for anything
+* (bug 19900) The "listgrouprights-key" message is now wrapped in a div with
+  class "mw-listgrouprights-key"
+* (bug 471) Allow RSS feeds for watchlist, using an opt-in security token
+* (bug 10812) Interwiki links can have names and descriptions, fetched from
+  message 'interwiki-desc-PREFIX', not really used anywhere yet though
+* (bug 9691) Add type (signup or login) parameter to
+  AuthPlugin::ModifyUITemplate()
+* (bug 14454) "Member of group(s)" in Special:Preferences causes language
+  difficulties
+* (bug 16697) Unicode combining characters are difficult to edit in some
+  browsers
+* Parser test supports uploading results to remote CodeReview instance
+* (bug 20013) Added CSS class "mw-version-ext-version" is wrapped on the
+  extension version in Special:Version
+* (bug 20014) Added CSS class "mw-listgrouprights-right-name" is wrapped on the
+  right name in Special:ListGroupRights
+* (bug 12920) New CoreParserFunction {{nse:...}} as an url-friendly equivalent
+  to {{ns:...}}
+* (bug 16322) Allow maintenance scripts to accept DB user/pass over input or
+  params
+* (bug 18566) Maintenance script to un/protect pages
+* (bug 671) The HTML <abbr> tag is now permitted.
+* RecentChanges now has a legend to explain what the Nmb! flags mean, and the
+  flags have tooltips.
+* (bug 15209) New hook BeforeInitialize called after everything has been setup
+  but before Mediawiki::performRequestForTitle()
+* wgMainPageTitle variable now available to JavaScript code to identify the main
+  page link, so it doesn't have to be extracted from the link URLs.
+* (bug 16836) Display preview of signature in user preferences and describe its
+  use
+* The default output format is now HTML 5 instead of XHTML 1.0 Transitional.
+  This can be disabled by setting $wgHtml5 = false;.  Specific features enabled
+  if HTML 5 is used:
+** Some extra inputs will be autofocused, in supporting browsers.
+** The summary attribute has been removed from tables of contents.  summary is
+   obsolete in HTML 5 and wasn't useful here anyway.
+** Unnecessary type="" attribute removed for CSS and JS.
+** If $wgWellFormedXml is set to false, some bytes will be shaved off of HTML
+   output by omitting some things like quotation marks where HTML 5 allows.
+** (bug 16921) maxlength enabled for page move comments
+* The description message in $wgExtensionCredits can be an array with parameters
+* New hook SpecialRandomGetRandomTitle allows extensions to modify the selection
+  criteria used by Special:Random and subclasses, or substitute a custom result,
+  deprecating the $wgExtraRandompageSQL config variable
+* (bug 20318) Distinct CSS classes for ISBN/RFC/PMID special links added
+* (bug 20404) Custom fields in the user creation form template can now have
+  detail labels in prefsectiontip divs.
+* MakeSysop and MakeBot are now aliases for Special:UserRights
+* IndexPager->mLimitsShown can now be an associative array of limit => text-to-
+  display-in-limit-form.
+* (bug 18880) LogEventsList::showLogExtract() can now take a string-by-reference
+  and add its HTML to it, rather than having to go straight to $wgOut.
+* Added $wgShowDBErrorBacktrace, to allow users to easily gather backtraces for
+  database connection and query errors.
+* Show change block / unblock link on Special:Contributions if user is blocked
+* Display note on Special:Contributions if the user is blocked, and provide an
+  excerpt from the block log.
+* (bug 19646) New hook: ImgAuthBeforeStream for tests and functionality before
+  file is streamed to user, but only when using img_auth
+* Note on non-existing user and user talk pages if user does not exist
+* New hook ShowMissingArticle so extensions can modify the output for
+  non-existent pages.
+* Admins could disable some variants using $wgDisabledVariants now. ONLY apply
+  on wikis enabled LanguageConverter.
+* (bug 16310) Credits page now lists IP addresses rather than saying the number
+  of anonymous users that edited the page
+* New permission 'sendemail' added. Default right for all registered users. Can
+  for example be used to prevent new accounts from sending spam.
+* (bug 16979) Tracking categories for __INDEX__ and __NOINDEX__
+* Two new hooks, ConfirmEmailComplete and InvalidateEmailComplete, which are
+  called after a user's email has been successfully confirmed or invalidated.
+* (bug 19741) Moved the XCF files out of the main MediaWiki distribution, for
+  a smaller subversion checkout.
+* (bug 13750) First letter capitalization can now be a per-namespace setting
+* (bug 21073) "User does not exist" message no longer displayed on sub-sub-pages
+  of existing users
+* (bug 21095) Tracking categories produced by the parser (expensive parser
+  function limit exceeded, __NOINDEX__ tracking, etc) can now be disabled by
+  setting the  system message ([[MediaWiki:expensive-parserfunction-category]]
+   etc) to "-".
+* Added maintenance script sqlite.php for SQLite-specific maintenance tasks.
+* Rewrote Special:Upload to allow easier extension.
+* Upload errors that can be solved by changing the filename now do not require
+  reuploading.
 * Added $wgRateLimitsExcludedIPs, to allow specific IPs to be whitelisted from
   rate limits.
-* (bug 14981) Shared repositories can now have display names, located at
-  Mediawiki:Shared-repo-name-REPONAME, where REPONAME is the name in 
-  $wgForeignFileRepos
-* Special:ListUsers: Sort list of usergroups by alphabet
-* (bug 16762) Special:Movepage now shows a list of subpages when possible
-* (bug 17585) Hide legend on Special:Specialpages from non-privileged users
-* Added $wgUseTagFilter to control enabling of filter-by-change-tag
-* (bug 17291) MediaWiki:Nocontribs now has an optional $1 parameter for the
-  username
-* Wrap special page summary message '$specialPageName-summary' into a div
-  with class 'mw-specialpage-summary'
-* $wgSummarySpamRegex added to handle edit summary spam. This is used *instead*
-  of $wgSpamRegex for edit summary checks. Text checks still use $wgSpamRegex.
-* New function to convert content text to specified language (only applies on wiki with
-  LanguageConverter class)
-* (bug 17844) Redirect users to a specific page when they log in, see 
-  $wgRedirectOnLogin
-* Added a link to Special:UserRights on Special:Contributions for privileged users
-* (bug 10336) Added new magic word {{REVISIONUSER}}, which displays the editor
-  of the displayed revision's author user name
-* LinkerMakeExternalLink now has an $attribs parameter for link attributes and 
-  a $linkType parameter for the type of external link being made
-* (bug 17785) Dynamic dates surrounded with a <span> tag, fixing sortable tables 
-  with dynamic dates.
-* (bug 4582) Provide preference-based autoformatting of unlinked dates with the 
-  dateformat parser function.
-* (bug 17886) Special:Export now allows you to export a whole namespace (limited 
-  to 5000 pages)
-* (bug 17714) Limited TIFF upload support now built in if 'tif' extension is
-  enabled. Image width and height are now recognized, and when using ImageMagick,
-  optional flattening to PNG or JPEG for inline display can be enabled by setting
-  $wgTiffThumbnailType
-* Renamed two input IDs on Special:Log from 'page' and 'user' to 'mw-log-page' and
-  'mw-log-user', respectively
-* Added $wgInvalidUsernameCharacters to disallow certain characters in
-  usernames during registration (such as "@")
-* Added $wgUserrightsInterwikiDelimiter to allow changing the delimiter
-  used in Special:UserRights to denote the user should be searched for
-  on a different database
-* Add a class if 'missingsummary' is triggered to allow styling of the summary
-  line
-
-=== Bug fixes in 1.15 ===
-
-* (bug 16968) Special:Upload no longer throws useless warnings.
-* (bug 17000) Special:RevisionDelete now checks if the database is locked
-  before trying to delete the edit.
-* (bug 16852) padleft and padright now handle multibyte characters correctly
-* (bug 17010) maintenance/namespaceDupes.php now add the suffix recursively if
-  the destination page exists
-* (bug 17035) Special:Upload now fails gracefully if PHP's file_uploads has
-  been disabled
-* Fixing the caching issue by using -{T|xxx}- syntax (only applies on wiki with
-  LanguageConverter class)
-* Improving the efficiency by using -{A|xxx}- syntax (only applies on wiki with
-  LanguageConverter class)
-* (bug 17054) Added more descriptive errors in Special:RevisionDelete
-* (bug 11527) Diff on page with one revision shows "Next" link to same diff
-* (bug 8065) Fix summary forcing for new pages
-* (bug 10569) redirects to Special:Mypage and Special:Mytalk are no longer
-  allowed by default. Change $wgInvalidRedirectTargets to re-enable.
-* (bug 3043) Feed links of given page are now preceded by standard feed icon
-* (bug 17150) escapeLike now escapes literal \ properly
-* Inconsistent use of sysop, admin, administrator in system messages changed
-  to 'administrator'
-* (bug 14423) Check block flag validity for block logging
-* DB transaction and slave-lag avoidance tweaks for Email Notifications
-* (bug 17104) Removed [Mark as patrolled] link for already patrolled revisions
-* (bug 17106) Added 'redirect=no' and 'mw-redirect' class to redirects at
-  "user contributions"
-* Rollback links on new pages removed from "user contributions"
-* (bug 15811) Re-upload form tweaks: license fields removed, destination locked,
-  comment label uses better message
-* Whole HTML validation ($wgValidateAllHtml) now works with external tidy
-* Parser tests no longer fail when $wgExternalLinkTarget is set in
-  LocalSettings
-* (bug 15391) catch DBQueryErrors on external storage insertion. This avoids
-  error messages on save were the edit in fact is saved.
-* (bug 17184) Remove duplicate "z" accesskey in MonoBook
-* Parser tests no longer fail when $wgAlwaysUseTidy is set in LocalSettings.php
-* Removed redundant dupe warnings on reupload for the same title. Dupe warnings
-  for identical files at different titles are still given.
-* Add 'change tagging' facility, where changes can be tagged internally with
-  certain designations, which are displayed on various summaries of changes,
-  and the entries can be styled with CSS.
-* (bug 17207) Fix regression breaking category page display on PHP 5.1
-* Categoryfinder utility class no longer fails on invalid input or gives wrong
-  results for category names that include pseudo-namespaces
-* (bug 17252) Galician numbering format
-* (bug 17146) Fix for UTF-8 and short word search for some possible MySQL
-  configs
-* (bug 7480) Internationalize database error message
-* (bug 16555) Number of links to mediawiki.org scaled back on post-installation
-* (bug 14938) Removing a section no longer leaves excess whitespace
-* (bug 17304) Fixed fatal error when thumbnails couldn't be generated for file
-  history
-* (bug 17283) Remove double URL escaping in show/hide links for log entries
-  and RevisionDeleteForm::__construct
-* (bug 17105) Numeric table sorting broken
-* (bug 17231) Transcluding special pages on wikis using language conversion no
-  longer affects the page title
-* (bug 6702) Default system messages updated/improved
-* (bug 17190) User ID on preference page no longer has delimeters
-* (bug 17341) "Powered by MediaWiki" should be on the left on RTL wikis
-* (bug 17404) "userrights-interwiki" right was missing in User::$mCoreRights
-* (bug 7509) Separation strings should be configurable
-* (bug 17420) Send the correct content type from action=raw when the HTML file 
-  cache is enabled.
-* (bug 12746) Do not allow new password e-mails when wiki is in read-only mode
-* (bug 17478) Fixed a PHP Strict standards error in
-  maintenance/cleanupWatchlist.php
-* (bug 17488) RSS/Atom links in left toolbar are now localized in classic skin
-* (bug 17472) use print <<<EOF in maintenance/importTextFile.php
-* Special:PrefixIndex: Move table styling to shared.css, add CSS IDs to tables
-  use correct message 'allpagesprefix' for input form label, replace _ with ' '
-  in next page link
-* (bug 17506) Exceptions within exceptions now respect $wgShowExceptionDetails
-* Fixed excessive job queue utilisation
-* File dupe messages for remote repos are now shown only once.
-* (bug 14980) Messages 'shareduploadwiki' and 'shareduploadwiki-desc' are now
-  used as a parameter in 'sharedupload' for easier styling and customization.
-* (bug 17482) Formatting error in Special:Preferences#Misc (Opera)
-* (bug 17556) <link> parameters in Special:Contributions feeds (RSS and Atom)
-  now point to the actual contributors' feed.
-* ForeignApiRepos now fetch MIME types, rather than trying to figure it locally
-* Special:Import: Do not show input field for import depth if
-  $wgExportMaxLinkDepth == 0
-* (bug 17570) $wgMaxRedirects is now correctly respected when following
-  redirects (was previously one more than $wgMaxRedirects)
-* (bug 16335) __NONEWSECTIONLINK__ magic word to suppress new section link.
-* (bug 17581) Wrong index name in PostgreSQL's updater: was rc_timestamp_nobot,
-  changed to rc_timestamp_bot
-* (bug 17437) Fixed incorrect link to web-based installer
-* (bug 17538) Use shorter URLs in <link> elements
-* (bug 13778) Hidden input added to the search form so that using the Enter key
-  on IE will do a fulltext search like clicking the button does
-* (bug 1061) CSS-added icons next to links display through the text and makes
-  it unreadable in RTL
-* Special:Wantedtemplates now works on PostgreSQL
-* (bug 14414) maintenance/updateSpecialPages.php no longer throws error with
-  PostgreSQL
-* (bug 17546) Correct Tongan language native name is "lea faka-Tonga"
-* (bug 17621) Special:WantedFiles has no link to Special:Whatlinkshere
-* (bug 17460) Client ecoding is now correctly set for PostgreSQL
-* (bug 17648) Prevent floats from intruding into edit area in previews if no
-  toolbar present
-* (bug 17692) Added (list of members) link to 'user' in Special:Listgrouprights
-* (bug 17707) Show file destination as plain text if &wpForReUpload=1
-* (bug 10172) Moved setting of "changed since last visit" flags out of the job
-  queue
-* (bug 17761) "show/hide" link in page history in now works for the first
-  displayed revision if it's not the current one
-* (bug 17722) Fix regression where users are unable to change temporary passwords
-* (bug 17799) Special:Random no longer throws a database error when a non-
-  namespace is given, silently falls back to NS_MAIN
-* (bug 17751) The message for bad titles in WantedPages is now localized
-* (bug 17860) Moving a page in the "MediaWiki" namespace using SuppressRedirect
-  no longer corrupts the message cache
-* (bug 17900) Fixed User Groups interface log display after saving groups.
-* (bug 17897) Fixed string offset error in <pre> tags
-* (bug 17778) MediaWiki:Catseparator can now have HTML entities
-* (bug 17676) Error on Special:ListFiles when using Postgres
-* Special:Export doesn't use raw SQL queries anymore
-* (bug 14771) Thumbnail links to individual DjVu pages have two no longer have
-  two "page" parameters
-* (bug 17972) Special:FileDuplicateSearch form now works correctly on wikis that
-  don't use PathInfo or short urls
-* (bug 17990) trackback.php now has a trackback.php5 alias and works with 
-  $wgScriptExtension
-* (bug 14990) Parser tests works again with PostgreSQL
-* (bug 11487) Special:Protectedpages doesn't list protections with pr_expiry
-  IS NULL
-* (bug 18018) Deleting a file redirect leaves behind a malfunctioning redirect
-* (bug 17537) Disable bad zlib.output_compression output on HTTP 304 responses
-* (bug 11213) [edit] section links in printable version no longer appear when 
-  you cut-and-paste article text
-* (bug 17405) "Did you mean" to mirror Go/Search behavior of original request
-* (bug 18116) 'edittools' is now output identically on edit and upload pages
-* (bug 17241) The diffonly URI parameter should cascade to "Next edit" and 
-  "Previous edit" diff links
-* (bug 16823) 'Sidebar search form should not use Special:Search view URL as 
-  target'
-* (bug 16343) Non-existing, but in use, category pages can be "go" match hits
-* Fixed the circular template inclusion check, was broken when the loop 
-  involved redirects. Without this, infinite recursion within the parser is
-  possible.
-* (bug 17611) Provide a sensible error message on install when the SQLite data
-  directory is wrong.
-* (bug 16937) Fixed PostgreSQL installation on Windows, workaround for upstream 
-  pg_version() bug.
-* (bug 11451) Fix upgrade from MediaWiki 1.2 or earlier (imagelinks schema).
-* Fixed SQLite indexes, installation and upgrade. Reintroduced it as an option 
-  to the installer.
+* (bug 21222) When $wgUseTeX is not enabled, <math> is no longer registered with
+  the parser so extensions are free to implement their own <math> tag
+* (bug 21047) Wrap 'cannotdelete' into a div with the generic 'error' class and
+  an own 'mw-error-cannotdelete' class
+* New hook AbortNewAccountAuto, called before account creation from AuthPlugin-
+  or ExtUser-driven requests.
+* (bug 3480) The warning saying that the page has a history when deleting it now
+  contains the number of revisions in the history
+* $wgStylePath and $wgLogo are now set in the default LocalSettings.php file.
+* (bug 20186) Allow filtering history for revision deletion.
+* New hook OtherBlockLogLink, called in Special:IPBlockList and Special:Block
+  to show links to block logs of other blocking extensions, i.e. GlobalBlocking
+* Added search capabilities to SQLite backend
+* rebuildtextindex.php maintenance script now supports databases other than
+  MySQL
+* upgrade1_5.php now requires to be run --update option to prevent confusion
+* (bug 17662) Customizable default preload/editintro for new sections in the
+  respective addsection-preload and addsection-editintro messages
+* Added maintenance script checkSyntax.php that checks for PHP syntax errors
+  and common coding mistakes
+* Updated Unicode normalization tables
+* (bug 21604) Spellcheck attribute for editsummary
+* New wgCategories JavaScript global variable for userscripts.
+* (bug 20717) Added checkboxes to hide users with bot and/or sysop group
+  membership in SpecialActiveusers
+* Allow \pagecolor and \definecolor in texvc
+* $wgTexvcBackgroundColor contains background color for texvc call
+* (bug 21574) Redirects can now have "303 See Other" HTTP status
+* EditPage refactored to allow extensions to derive new edit modes much easier.
+* (bug 21826) Subsections of Special:Version now also have anchors
+* (bug 19791) Add URL of file source as comment to thumbs (for ImageMagick)
+* (bug 21946) Sorted wikitables do not properly handle minus signs
+* (bug 18885) Red links for media files do not support shared repositories
+* Added $wgFixArabicUnicode, to convert deprecated presentation forms in
+  Arabic text to their modern equivalents, and $wgFixMalayalamUnicode, to
+  convert ZWJ-based chillu sequences in Malayalam text to their Unicode 5.1
+  equivalents.
+* (bug 22051) Returing false in SpecialContributionsBeforeMainOutput hook now
+  stops normal output
+* Send new password e-mail in users preference language
+* LanguageConverter now support nested using of manual convert syntax like
+  "-{-{}-}-"
+* Upload license preview now uses the API instead of action=ajax
+* (bug 7346) Add <guid> to RSS to avoid duplicates
+* (bug 19996) Added new hooks for Special:Search, which allow to further
+  restrict/expand it.
+* (bug 21936) When a revision has been patrolled, there's now a link back to the
+  article
+* (bug 22315) SpecialRecentChangesQuery hook now pass $query_options and checks
+  the return value
+* Separate unit test suites under t/ and tests/ were merged and moved to
+  maintenance/tests/.
+* importImages.php maintenance script can now use the original uploader and 
+comment from another wiki.
+* Support for Turck MMCache was removed
+* (bug 14592) Warn users when they try to move their user page that their
+  account will not be renamed
+* Show block log on non-existing user (talk) pages of currently blocked users
+
+=== Bug fixes in 1.16 ===
+
+* (bug 18031) Make namespace selector on Special:Export remember the previous
+  selection
+* The svn-version version numbers on Special:Version have been removed
+* (bug 17374) Special:Export no longer exports two copies of the same page
+* (bug 18190) Proper parsing in MediaWiki:Sharedupload message
+* (bug 17617) HTML cleanup for ImagePage
+* (bug 17964) namespaceDupes.php no longer fails on an empty interwiki table
+* Improved error handling for image moving
+* (bug 17974) On Special:SpecialPages, restricted special pages are now marked
+  with <strong> tags, helps with text-based browsers
+* (bug 18259) Special:DeletedContributions now also uses
+  MediaWiki:Sp-contributions-logs for the link to Special:Log
+* Don't add empty title="" attributes to links to anchors on the current page
+* (bug 18291) rebuildrecentchanges.php failed to add deletion log entries
+* (bug 18304) rebuildrecentchanges.php got size changes wrong
 * (bug 18170) Fixed a PHP warning in Parser::preSaveTransform() in PHP 5.3
+* (bug 18289) Database connection error page now returns correct HTML
+* "successbox", "errorbox" and related CSS classes are now available in all
+  skins
+* (bug 18316) Removed superfluous name="fulltext" from Special:Search
+* (bug 18331) MediaWiki:Undelete-revision can now have wikitext
+* The "noautoblock" flag is no longer displayed in the block log when blocking
+  an IP address
+* (bug 18009) $wgHooks and $wgExtensionFunctions now support closures
+* (bug 17948) Maintenance scripts now exit(0) or exit(1) as appropriate
+* (bug 18377) Time in Enhanced ChangesList lacking localisation
+* (bug 12998) Allow <sup>, <sub>, etc. in DISPLAYTITLE
+* (bug 1553) Lowercase navigation headings in German
+* (bug 7830) Pending transactions failed to commit on loginToUse() error
+* (bug 11613) session.save_handler being over-ridden
+* (bug 11381) session.save_handler being set twice (causes error)
+* (bug 17835) ForeignAPIRepo throwing error on first page load for file
+* (bug 18115) ForeignAPIRepo cache isn't working
+* Fixed a bug caused by LanguageConverter.php, which brings an abnormal '}-'
+  after some parsed math syntax.
+* (bug 18441) rebuildrecentchanges.inc no longer ignores $wgLogRestrictions
+* (bug 18317) Bolded selections in 1 | 3 | etc days on RecentChanges now use
+  <strong> instead of hardcoded styles
+* (bug 18449) Fixed items number per column on category pages when the total is
+  divisible by 3
+* (bug 18121) maintenance/deleteArchivedRevisions.php no longer deletes
+  revisions when --delete is not passed
+* (bug 13172) GPS coordinates in image Exif data are now actually displayed
+* Overhaul of preferences system, includes the following bug fixes:
+** (bug 5363) Changes to default preferences now impact registered users.
+** (bug 14806) Hook to enable putting preferences in existing tabs.
+** (bug 17191) Registration date now listed on preferences page.
+** The user_properties table (now used for storing preferences) has been added
+   to $wgSharedTables.
+** Note that this change will break some extensions which have not been adapted
+   for it.
+* (bug 17020) Adding fallback encodings for Traditional and Simplified Chinese
+  languages while the the text is typed as URLs.
+* (bug 17614) Prev / Next links are not shown if all results are shown
+* (bug 18207) Strange spacing before [[irc:...]] links
+* Removed float from the user login form in RTL interface - caused display
+  problems in FF2
+* (bug 15008) Redirect images are now subject to Bad image list rules
+* (bug 6802) profileinfo.php now also work on other database servers than MySQL
+* (bug 16925) Diffs no longer fail when $wgExternalDiffEngine is set to
+  'wikidiff' or 'wikidiff2' but extension is not installed
+* (bug 18326) Chmod errors in file repos have been hidden
+* (bug 18718) Comma after a } create a error in IE
+* (bug 18716) Removed redundant class in Modern skin CSS for category links and
+  tweaked spacing.
+* (bug 18656) Use proper directory separators in wfMkdirParents()
+* (bug 18549) Make Special:Blockip respect $wgEnableUserEmail and
+  $wgSysopEmailBans
+* (bug 16912) Tooltips on images with link= disappear
+* (bug 18389) Localise numbers in EXIF data
+* (bug 18522) Wrap MediaWiki:Protect-cascadeon in a div for identification
+* (bug 18438) Tweak HTML for preview bar for consistency and accessibility
+* (bug 18432) Updated documentation for dumpBackup.php
+* Fix array logic in Sanitizer::removeHTMLtags so that it doesn't strip good
+  tags that were redundantly defined.
+* (bug 14118) SpecialPage::getTitleFor does not return a localised name
+* (bug 18698) Renaming non entry point maintenance scripts from .inc.php to
+  .inc
+* Deprecated methods Title::getInterwikiLink, Title::userCanCreate(),
+  Title::userCanEdit() and Title::userCanMove() have been removed
+* Only show upload links on file description if $wgEnableUploads = true
+  and user can upload
+* Don't say "You need to log in to upload/move", because it's possible that
+  uploading/moving is disabled for registered users as well (e.g. only sysops)
+* (bug 18943) Handle invalid titles gracefully at Special:Mostlinked
 * (bug 8873) Enable variant conversion in text on 'alt' and 'title' attributes
+* (bug 10837) Introducing the StubUserVariant class to determine the variant
+  variable instead of using this to overrules the user language preference.
+* (bug 19014) If user had deletedhistory right, but not undeleted right, then
+  show "view" instead of "view/restore" on logs.
+* (bug 19017) TOC level calculation error in an odd case
+* (bug 18999) CSS update for RTL interwiki links
+* (bug 18925) history.js removes class names of list elements on initialization
+* Multiple whitespace in TOC anchors is now stripped, for consistency with the
+  link from the edit comment
+* (bug 19112) Preferences now respects $wgUseExternalEditor
+* (bug 18173) MediaWiki now fails when unable to determine a client IP
+* (bug 19170) Special:Version should follow the content language direction
+* (bug 19160) maintenance/purgeOldText.inc is now compatible with PostgreSQL
+* Fixed performance regression in "bad image list" feature
+* Show user preference 'Use live preview' if $wgLivePreview is enabled only
+* (bug 17014) Blocked users can no longer use Special:UserRights unless they
+  can add/remove *all* groups (have 'userrights' permission).
+* (bug 19294) Always show Sp-contributions-footer(-anon)
+* Attempts to restrict reading of pages while anonymous viewing is allowed
+  via extensions not using the userCan hook and via $wgRevokePermissions now
+  work.
+* (bug 8445) Multiple-character search terms are now handled properly for
+  Chinese
+* (bug 19450) Use formatNum for "Number of edits" in Special:Preferences
+* (bug 11242) Check for MySQL storage engines during installation now checks
+  whether the engines are actually available
+* (bug 19390) Omit the "printable version" link on the printable version
+* (bug 18394) img_auth.php now respects userCan
+* (bug 19509) Uploading to a file named '0' previously treated it as null input
+   and attempted to upload with the source name. Now warns about not having an
+   extension (since 0.ext is perfectly valid)
+* (bug 19468) Enotif preferences are now only displayed when they are turned on
+* (bug 19442) Show/hide options on watchlist only work once
+* (bug 19602) PubMed Magic links now use updated NIH url
+* (bug 19637) externallinks have links to self
+* Don't load Opera 9.5 RTL fixes for Opera 9.6
+* Remove five-year-old KHTMLFixes.css, which is unlikely to be relevant anymore
+  and was causing problems.
+* Removed repetition of URIs in the title attributes of external links.
+* (bug 19693) User name is now escaped in "Contributions for ..." link on
+  Special:BlockIP
+* (bug 19571) Override buildConcat for SQLite.
+* Log in and log out links no longer return to page view when clicked from
+  history view, edit page, or something similar
+* (bug 19513) RTL fixes for new Search UI
+* (bug 16497) Special:Allmessages is paginated
+* (bug 18708) CSS plainlinks class now available to all skins
+* (bug 19590) Database error messages no longer have "MySQL" hardcoded as the
+  database type
+* (bug 19759) successbox on Special:Preferences now correctly aligned on
+  standard, nostalgia and cologneblue skin
+* (bug 19814) interwiki links from file links ([[File:Foo.jpg|link=de:Test]])
+  are no longer recorded in the pagelinks table
+* (bug 19784) date option "ISO 8601" produced illegal id
+* (bug 19761) Removed autogenerated <meta keywords> tag with link data.
+  Keyword set was not useful, and is ignored by modern search engines anway.
+* (bug 19827) Special:SpecialPages title is "Upload file
+* (bug 19355) Added .xhtml, .xht to upload file extension blacklist
+* (bug 19287) Workaround for lag on history page in Firefox 3.5
+* (bug 19564) Updated docs/hooks.txt
+* (bug 18751) Fix for buggage in profiling setup for some extensions on PHP 5.1
+* (bug 17139) ts_resortTable inconsistent trimming makes date sorting fragile
+* (bug 19445) Change oldimage table to use ON UPDATE CASCADE for FK to image
+  table.
+* (bug 14080) Short notation links to subpages didn't work in edit summaries
+* (bug 17374) Special:Export no longer exports multiple copies of pages
+* (bug 19818) Edits to user CSS/JS subpages can now be marked as patrolled by
+  users who can't edit them
+* (bug 19839) Comments in log items are no more double escaped
+* (bug 18161) Fix inconsistent separators in watchlist link toolbars with
+  "enhanced recent changes"
+* (bug 16877) Moving a page over a redirect no longer leaves an orphan entry in
+  the recentchanges table
+* (bug 16009) Limit selection forms based on Pager now links to the correct page
+  when using long urls
+* The display of the language list on the preferences is more comply with the
+  BCP 47 standards.
+* (bug 19849) Custom X-Vary-Options header now disabled unless $wgUseXVO is set
+* (bug 19301) Duplicates entries in $wgAddGroups, $wgRemoveGroups,
+  $wgGroupsAddToSelf and $wgGroupsRemoveFromSelf are no more displayed on
+  Special:ListGroupRights
+* (bug 18799) Special:Userlogin now handles correctly the returnto parameter
+  to not link back to Special:Userlogout when user's language isn't the same as
+  content's language
+* (bug 19479) Show proper error message when unable to connect to PostgreSQL
+  database with username/password in MediaWiki's setup
+* (bugs 18407, 18409) Special:Upload is now listed on Special:Specialpages only
+  if uploads are enabled and the user can access it
+* (bug 17988) Spaces before [[Category:]] links are no longer ignored
+* (bug 19957) All known-failing tests now marked disabled; added --run-disabled
+  option to parser test suite to run disabled tests if desired.
+* (bug 16311) Make recent change flags (n/m/b) <abbr>s instead of <span>s
+* (bug 15680) Split the edit tip message of user CSS/JS subpage into
+  "usercssyoucanpreview" and "userjsyoucanpreview" respectively.
+* (bug 12110) Split the rights for editing users' CSS/JS subpage from
+  "editusercssjs" into "editusercss" and edituserjs" respectively.
+* (bug 19394) RecentChanges feed URLs for log items with no revisions
+  (eg Newuser, Userrights) are no longer broken
+* (bug 17395) Remote file descriptions use user language ($wgLang), not wiki
+  language ($wgContLang)
+* (bug 11867) Lock error on redirect table when running orphans.php
+* (bug 18930) initStats.php now refreshes active users count
+* (bug 18699) Using the nosummary URL option no longer triggers the "You have
+  not provided a summary" warning for those who activated it in their
+  preferences
+* (bug 18855) commandLine.inc and Maintenance.php are now properly included
+  using the full path
+* (bug 18497) Fixed broken style sheets in Opera fullscreen mode
+* (bug 16084) Default memory limit has be increased to 50M, see $wgMemoryLimit
+* (bug 17864/19519) Added proper input normalization in Special:UserRights
+* (bug 20086) Add Hook to add extra statistics at the end of Special:Statistics
+* (bug 19289) importDump.php can now handle bzip2 and 7zip
+* (bug 20131) Fixed a PHP notice for users having the "rollback" right on
+  Special:RecentChangesLinked
+* Do not transform EXIF fields with pure text to avoid results like
+  foo,bar@example,com
+* (bug 20176) Fix login/logout links in skin CologneBlue
+* (bug 20203) "Powered by Mediawiki" now has height/width on image tag
+* (bug 20273) Fix broken output when no pages are found in the content
+  namespaces
+* (bug 20265) Make AncientPages and UnusedFiles work on SQLite
+* Fixed XSS vulnerability for Internet Explorer clients (only pre-release
+  versions of MediaWiki were affected).
+* (bug 14817) Moving a page to a subpage of itself moves it twice
+* (bug 20289) $wgMaximumMovedPages should only count pages actually moved
+* (bug 15248) Non-breaking spaces and certain other Unicode space characters
+  are now normalized to ordinary spaces in titles; if your wiki has existing
+  titles with such characters, run cleanupTitles.php and/or cleanupImages.php
+* (bug 11143) Links containing invalid UTF-8 percent-code sequences are now
+  cleanly disabled instead of breaking parsing entirely on PHP 5.2.
+* (bug 20296) Fixed an PHP warning in Language::getMagic() in PHP 5.3
+* (bug 20358) Unprotect tab was missing accesskey; now same as protect tab.
+* (bug 20317) Cleaned up default main page link accesskey settings
+* (bug 20362) Special:Statistics now produces valid HTML when view counters are
+  enabled
+* (bug 19857) maintenance/deleteRevision.php on last revision no longer breaks
+  target page
+* (bug 20365) Page name with with c/g/h/j/s/u + x are now correctly handled in
+  Special:MovePage with Esperanto as content language
+* (bug 20364) Fixed regression in GIF metadata loading
+* (bug 20299) MediaWiki:Move-subpages and MediaWiki:Move-talk-subpages can now
+  use wikitext
+* (bug 15475) DatabaseBase::setFlag(), DatabaseBase::clearFlag() and
+  DatabaseBase::getFlag() now have documentation
+* (bug 19966) MediaWiki:License-header is now used for the licensing header in
+  the file description page instead of MediaWiki:License
+* (bug 20380) Links to history/deleted edits at the top of
+  Special:RevisionDelete are no more displayed when when doing log suppression
+* (bug 8143) Localised parser function names are now correctly case insensitive
+  if they contain non-ASCII characters
+* (bug 19055) maintenance/rebuildrecentchanges.php now purges
+  Special:Recentchanges's RSS and Atom feed cache
+* The installer will now try to bypass PHP's max_execution_time
+* (bug 20260) SQLite no longer tries to automatically create the database at
+  execution time, this now happens only at install time; if it is not available
+  at script execution, it now throws an exception
+* Fixed EditFilterMerged hook so the hookError parameter serves a purpose
+  (analogous to EditFilter hook)
+* (bug 2257) Tag extensions can expand template parameters provided to the tag,
+  by using a new parameter added to the recursiveTagParse function
+* (bug 14900) __INDEX__ and __NOINDEX__ no longer override site config set in
+  $wgArticleRobotPolicies.
+* (bug 20466) Hidden categories are no more displayed when printing
+* (bug 20446) When changing user rights with User@remotewiki and remotewiki is
+  the local wiki, the user is now treated as the local user
+* (bug 20494) OutputPage::getArticleBodyOnly() no longer requires an useless
+  argument
+* (bug 20136) Protection form JavaScript now synchronizes the expiry boxes on
+  any change, in addition to onkeyup.
+* Don't link to "edit this page" on MediaWiki:Noarticletext if user is not
+  allowed to create page. Done via new message
+  MediaWiki:Noarticletext-nopermission
+* Improved compatibility between the Vector skin and addPortletLink() from
+  wikibits.js: empty portlets are now present but hidden, adding an element to a
+  portlet unhides it
+* (bug 19531) addPortletLink() now wraps inserted labels in a <span> element to
+  be compatible with the CSS for the Vector skin
+* (bug 20578) Wrong localized image metadata - duplicated string?
+* (bug 20556) Stub threshold's "other" <input> in Special:Preferences now has a
+  correct type="text" parameter
+* (bug 482) Don't include TOC in the printable version if it has been hidden
+* Adjust the time according to the user configuration on Special:Revisiondelete
+* (bug 20624) Installation no longer allows "qqq" as the chosen language
+* (bug 20634) The installer-created database user will now have all rights on
+  the database so that upgrades will go more smoothly.
+* (bug 18180) Special:Export ignores limit, dir, offset parameters
+* User::getBlockedStatus() works for all kinds of user objects and doesn't
+  assume the user object is equal to the current-user object ($wgUser)
+* (bug 20517) Cancel link from edit page now returns to the old version when
+  editing an old version
+* (bug 16902) Installer no longer shows warnings when exec() has been disabled
+  by disable_functions
+* (bug 20726) Title::getLatestRevID's documentation now says that the function
+  returns false if the page doesn't exist
+* (bug 20751) ForeignApiRepo now urldecodes filenames when saving to local cache
+* (bug 20730) Fix to Special:Version ViewVC link for branch checkouts
+* (bug 20353) wfShellExec() was adding extra quotes on Windows Vista, causing
+  command line scripts to fail
+* (bug 20702) Parser functions can now be used correctly in
+  MediaWiki:Missing-article
+* (bug 14117) "redirected from" is now also shown on foreign file redirects
+* (bug 17747) Only display thumbnail column in file history if the image can
+  be rendered.
+* (bug 3421) Live preview no longer breaks user CSS/JS previews
+* (bug 11264) The file logo on a file description page for documents (PDF, ...)
+  now links to the file rather than the file description page
+* Password fields built with HTMLForm now still have the type="password"
+  attribute if $wgHtml5=false.
+* (bug 20836) Preload now works for MediaWiki namespace
+* (bug 20885) Search box no longer suggests unavailable special pages
+* (bug 20948) "Create this page" on Special:Search is no longer displayed when
+  searching for special pages
+* (bug 20524) Hideuser: Show nice error when trying to block hidden user without
+  hideuser right
+* (bug 21026) Fixed file redirects on shared repos on non-English client wikis
+* (bug 21030) Fixed schema choices from being overwritten by defining unique
+  field names per driver.
+* (bug 21115) wgCanonicalSpecialPageName javascript variable is now always
+  false on non-special pages
+* (bug 21113) "Other statistics" header on Special:Statistics is no more
+  displayed when there isn't any entry in it
+* (bug 21114) Special:Contributions no longer shows diff links for new
+  revisions
+* (bug 21116) MediaWiki:Templatesused, MediaWiki:Templatesusedpreview and
+  MediaWiki:Templatesusedsection now support plural
+* (bug 21079) There is no more line wrapping between label and field in
+  Special:Log
+* (bug 20256) Fixed SQL errors on Special:Recentchanges and
+  Special:Recentchangeslinked on SQLite backend
+* (bug 20880) Fixed updater failure on SQLite backend
+* (bug 21182) Fixed invalid HTML in Special:Listgrouprights
+* (bug 20242) Installer no longer promts for user credentials for SQLite
+  databases
+* (bug 20911) Installer failed to create a SQLite database
+* (bug 20847) Deprecated deprecated akeytt() removed in wikibits.js leaving
+  dummy
+* (bug 21161) Changing $wgCacheEpoch now always invalidates file cache
+* (bug 20268) Fixed row count estimation on SQLite backend
+* (bug 20275) Fixed LIKE queries on SQLite backend
+* (bug 21234) Moving subpages of titles containing \\ now works properly
+* (bug 21006) maintenance/updateArticleCount.php now works again on PostgreSQL
+* (bug 19319) Add activeusers-intro message at top of SpecialActiveUsers page
+* (bug 21255) Fixed hostname construction for DNSBL checking
+* (bug 18019) Users are now warned when moving a file to a name in use on a
+  shared repository and only users with the 'reupload-shared' permission can
+  complete the move.
+* (bug 18909) Add missing Postgres INSERT SELECT wrapper
+* User::isValidPassword now only returns boolean results,
+  User::getPasswordValidity can be used to get an error message string
+* The error message shown in Special:ChangePassword now parses wiki markup
+* (bug 19859) Removed experimental HTMLDiff feature
+* Removed section edit links in edit conflict form
+* Allow SpecialActiveusers to work on non-MySQL databases
+* (bug 6579) Fixed protecting images from uploading only
+* (bug 18609) Search index was empty for some pages
+* (bug 13453) rebuildrecentchanges maintenance script works on PG again
+* (bug 16583) Reduce false positives when checking for PHP (on upload, etc.)
+* (bug 20112) Bitrotted tests in the t/ directory were failing.
+* (bug 21470) MediaWiki:Sp-contributions-explain is now wrapped in a <p> with
+  id "mw-sp-contributions-explain"
+* (bug 19159) Fixed \overleftrightarrow in texvc
+* (bug 19391) Fix caching for Recent ChangesFeed.
+* (bug 21455) Fixed "Watch this page" checkbox appearing on some special pages
+  even to non-logged in users
+* (bug 21551) Rewrote the Squid purge HTTP client to provide a more robust and
+  general implementation of HTTP, allowing it to purge non-Squid caches such as
+  Varnish.
+* Fixed corruption of long UDP debug log messages by using socket_sendto()
+  instead of fsockopen() with fwrite().
+* (bug 16884) Fixed feed links in sidebar not complying with URL parameters
+  of the displayed page
+* (bug 21403) memcached class renamed to MWMemecached to avoid conflict with
+  PHP's memcached extension
+* (bug 21650) Both calls to SkinTemplateTabs hook are now compatible
+* (bug 21672) Add missing Accept-Language to both Vary and XVO headers
+* (bug 21679) "Edit block reasons" link at the bottom of Special:Blockip is now
+  only displayed to the users that have "editinterface" right
+* (bug 21740) Attempting to protect a page that doesn't exist (salting) returns
+  "unknown error"
+* (bug 18762) both redirects and links get fixed one after another if
+  redirects-only switch is not present
+* (bug 20159) thumbnails rerendered if older that $wgThumbnailEpoch
+* Fixed a bug which in some situations causes the job queue to grow forever,
+  due to an infinite loop of job requeues.
+* (bug 21523) File that can have multiple pages (djvu, pdf, ...) no longer have
+  the page selector when they have only one page
+* (bug 21559) "logempty" message is now wrapped in a div with class
+  "mw-warning-logempty" when used in log extract
+* (bug 20549) Parser tests were broken on SQLite backend
+* (bug 21776) Interwiki urls like http://en.wikibooks.org/wiki/cs: should give
+  a redirect instead of a baderror.
+* (bug 21803) Special:MyContributions now keeps the query string parameters
+* Redirecting special pages now keep query string paramters set to "0" (e.g.
+  for namespace)
+* (bug 20765) Special:ListGroupRights no longer misses addables and removables
+  groups if there are duplicate entries
+* (bug 21814) Message shown when rolling back an edit with a deleted username
+  now shows '(username deleted)' instead of broken user tool links
+* (bug 21536) Fixed JavaScript error on Special:Search caused by an incorrect ID
+* (bug 21535) RecentChanges RSS feed now always recognises the namespace filter,
+  previously it sometimes didn't due to caching.
+* (bug 20388) ProfilerSimpleText no longer outputs comment on action=raw
+* refreshLinks.php now purges orphaned redirect table rows
+* (bug 2971) Swap links of hist & diff location on Special:Contributions for
+  consistency with RC/WL
+* (bug 21986) Special page names were are now capitalized by content language
+* If two log type have the same description, they're now both displayed in the
+  type selector on Special:Log
+* (bug 20115) Special:Userlogin title says "Log in / create account" even if the
+  user can't create an account
+* (bug 2658) Don't attempt to set the TZ environment variable.
+* (bug 9794) User rights log entries for foreign user now links to the foreign
+  user's page if possible
+* (bug 14717) Don't load nonexistent CSS fix files for non-Monobook skins
+* (bug 22034) Use wfClientAcceptsGzip() in wfGzipHandler instead of
+  reimplementing it.
+* (bug 19226) First line renders differently on many UI messages.
+* (bug 21303) Comments are no longer stripped from MediaWiki:Common.js and
+  skin-specific JS pages
+* (bug 5061) Use the more precise thumbcaption thumbimage and thumbinner classes
+  for image divs.
+* Fixed bug involving unclosed "-{" markup in the language converter
+* (bug 21870) No longer include Google logo from an external server on wiki error.
+* (bug 22181) Do not truncate if the ellipsis actually make the string longer
+* (bug 16039) Text disappearing after a bad image
+* (bug 18784) Internal links like [[File:Foo|caption]] should read 'caption',
+  not 'File:Foo' when Foo is not an image
+* (bug 21518) Special:UserRights no longer displays the user name box for users
+  that can only change their rights
+* (bug 21593) Special:UserRights now lists automatic groups membership
+* (bug 22364) Setting $wgUseExternalEditor to false no longer hides the reupload
+  link from file pages
+* Fix bug introduced in MediaWiki 1.12: The author field in
+  $wgExtensionCredits is no longer sorted with sort() but rather used
+  as it appears in extensions as was the case before r30117 where it
+  was unintentionally sorted along with other fields.
+* (bug 19334) Textarea no longer jumps when editing longer articles in IE8
+* Truncate summary of page moves in revision comment field to avoid broken
+  multibyte characters
+* (bug 22540) ForeignApiRepos no longer try to store thumbnails that don't exist
+* (bug 22551) Special:Resetpass now has a "Cancel" button that sends the user to 
+  the page set in the &returnto parameter.
+* (bug 19194) Search box in Modern skin doesn't focus with Safari/Chrome
+* (bug 17790) Users instantly logged off on HughesNet
+
+== API changes in 1.16 ==
 
-== API changes in 1.15 ==
-
-* (bug 16858) Revamped list=deletedrevs to make listing deleted contributions
-  and listing all deleted pages possible
-* (bug 16844) Added clcategories parameter to prop=categories
-* (bug 17025) Add "fileextension" parameter to meta=siteinfo&siprop=
-* (bug 17048) Show the 'new' flag in list=usercontribs for the revision that
-  created the page, even if it's not the top revision
-* (bug 17069) Added ucshow=patrolled|!patrolled to list=usercontribs
-* action=delete respects $wgDeleteRevisionsLimit and the bigdelete user right
-* (bug 15949) Add undo functionality to action=edit
-* (bug 16483) Kill filesort in ApiQueryBacklinks caused by missing parentheses.
-  Building query properly now using makeList()
-* (bug 17182) Fix pretty printer so URLs with parentheses in them are
-  autolinked correctly
-* (bug 17224) Added siprop=rightsinfo to meta=siteinfo
-* (bug 17239) Added prop=displaytitle to action=parse
-* (bug 17317) Added watch parameter to action=protect
-* (bug 17007) Added export and exportnowrap parameters to action=query
-* (bug 17326) BREAKING CHANGE: Changed output format for iiprop=metadata
-* (bug 17355) Added auwitheditsonly parameter to list=allusers
-* (bug 17007) Added action=import
-* BREAKING CHANGE: Removed rctitles parameter from list=recentchanges because
-  of performance concerns
-* Listing (semi-)deleted revisions and log entries as well in prop=revisions
-  and list=logevents
-* (bug 11430) BREAKING CHANGE: Modules may return fewer results than the
-  limit and still set a query-continue in some cases
-* (bug 17357) Added movesubpages parameter to action=move
-* (bug 17433) Added bot flag to list=watchlist&wlprop=flags output
-* (bug 16740) Added list=protectedtitles
-* Added mainmodule and pagesetmodule parameters to action=paraminfo
-* (bug 17502) meta=siteinfo&siprop=namespacealiases no longer lists namespace
-  aliases already listed in siprop=namespaces
-* (bug 17529) rvend ignored when rvstartid is specified
-* (bug 17626) Added uiprop=email to list=userinfo
-* (bug 13209) Added rvdiffto parameter to prop=revisions
-* Manual language conversion improve: Now we can include both ";" and ":" in
-  conversion rules
-* (bug 17795) Don't report views count on meta=siteinfo if $wgDisableCounters 
-  is set
-* (bug 17774) Don't hide read-restricted modules like action=query from users
-  without read rights, but throw an error when they try to use them.
-* Don't hide write modules when $wgEnableWriteAPI is false, but throw an error
-  when someone tries to use them
-* BREAKING CHANGE: action=purge requires write rights and, for anonymous users,
-  a POST request
-* (bug 18099) Using appendtext to edit a non-existent page causes an interface
-  message to be included in the page text
-* (bug 18601) generator=backlinks returns invalid continue parameter
-* (bug 18597) Internal error with empty generator= parameter
-* (bug 18617) Add xml:space="preserve" attribute to relevant tags in XML output
-
-=== Languages updated in 1.15 ===
-
-MediaWiki supports over 300 languages. Many localisations are updated
+* Added uiprop=changeablegroups to meta=userinfo
+* Added usprop=gender to list=users
+* (bug 18311) action=purge now works for images too
+* Add parentid to prop=revisions output
+* (bug 17832) action=delete returns 'unknownerror' instead of 'permissiondenied'
+  when the user is blocked
+* (bug 18546) Added timestamp of new revision to action=edit output
+* (bug 18554) Also list hidden revisions in list=usercontribs for privileged
+  users
+* (bug 13049) "API must be accessed from the primary script entry point" error
+* (bug 16422) Don't display help for format=jsonfm unless specifically requested
+* Added PHP and database version to meta=siteinfo output
+* (bug 18533) Add readonly message to meta=siteinfo output
+* (bug 18518) Add clprop=hidden to prop=categories
+* (bug 18710) Fixed internal error with empty parameter in action=paraminfo
+* (bug 18709) Missing descriptions for some parameters in action=paraminfo
+  output
+* (bug 18731) Show correct SVN links for extension modules in api.php?version
+* (bug 18730) Add version information to action=paraminfo output
+* (bug 18743) Add ucprop=size to list=usercontribs
+* (bug 18749) Add generator flag to action=paraminfo output
+* Make action=block respect $wgEnableUserEmail and $wgSysopEmailBans
+* Made deleting file description pages without files possible
+* (bug 18773) Add content flag to siprop=namespaces output
+* (bug 18785) Add siprop=languages to meta=siteinfo
+* (bug 14200) Added user and excludeuser parameters to list=watchlist and
+  list=recentchanges
+* Added index, fromtitle and byteoffset fields to action=parse&prop=sections
+  output
+* (bug 19313) action=rollback returns wrong revid on master/slave setups
+* (bug 19323) action=parse doesn't return section tree on pages with Cite
+  warnings
+* (bug 18720) Add anchor field to action=parse&prop=sections output
+* (bug 19423) The initial file description page used caption in user lang
+  rather than UI lang
+* (bug 17809) Add number of users in user groups to meta=siteinfo
+* (bug 18533) Add readonly reason to readonly exception
+* (bug 19528) Added XSLT parameter to API queries in format=xml
+* (bug 19040) Fix prependtext and appendtext in combination with section
+  parameter in action=edit
+* (bug 19090) Added watchlist parameter, deprecated watch and unwatch
+  parameter in action=edit
+* Added fields to list=search output: size, wordcount, timestamp, snippet
+* Where supported by backend, list=search adds a 'searchinfo' element with
+  optional info: 'totalhits' count and 'suggestion' alternate query term
+* (bug 19907) $wgCrossSiteAJAXdomains added to allow specified (or all)
+  external domains to access api.php via AJAX, if the browser supports the
+  Access-Control-Allow-Origin HTTP header
+* (bug 19999) Made metadata and properties of search results optional. Added
+  srprop and srinfo.
+* (bug 20700) Add amprop=default to meta=allmessages to list default value for
+  customized messages
+* Don't parse magic words in meta=allmessages, output messages unparsed
+* (bug 21105) list=usercontribs can now list contribs for User:0
+* (bug 21085) list=deletedrevs no longer returns only one revision when
+  drcontinue param is passed
+* (bug 21106) Deprecated parameters now tagged in action=paraminfo
+* (bug 19004) Added support for tags
+* (bug 21083) list=allusers no longer returns current timestamp for users
+  without registration date
+* (bug 20967) action=edit allows creation of invalid titles
+* (bug 19523) Add inprop=watched to prop=info
+* (bug 21589) API: Separate summary and initial page text for uploads
+* (bug 21817) list=usercontribs returns empty result for empty ucuser
+* (bug 21441) meta=userinfo&uiprop=options no longer returns default options
+  for logged-in users under certain circumstances
+* (bug 21945) Add chomp control in YAML
+* Expand the thumburl to an absolute url to make it consistent with url and
+  descriptionurl
+* (bug 20233) ApiLogin::execute() doesn't handle LoginForm :: RESET_PASS
+* (bug 22061) API: add prop=headitems to action=parse
+* (bug 22240) API: include time in siteinfo
+* (bug 22241) Quick edit is still using the deprecated watch parameter (API: Setting default for watch/unwatch wrongly set)
+* (bug 22245) blfilterredirect=nonredirects in blredirect mode wrongly filtering
+* (bug 22248) Output extension URLs in meta=siteinfo&siprop=extensions
+* Support key-params arrays in 'descriptionmsg' in meta=siteinfo&siprop=extensions
+* (bug 21922) YAML output should quote asterisk when used as key
+* (bug 22297) safesubst: to allow substitution without breaking transclusion
+* (bug 18758) API read of watchlist's wl_notificationtimestamp
+* (bug 20809) Expose EditFormPreloadText via the API
+* (bug 18427) Comment (edit summary) parser option for API
+* (bug 18608) API should provide list of CSS styles to apply to rendered output
+* (bug 18771) List possible errors in action=paraminfo
+
+=== Languages updated in 1.16 ===
+
+MediaWiki supports over 330 languages. Many localisations are updated
 regularly. Below only new and removed languages are listed, as well as
 changes to languages because of MediaZilla reports.
 
-* Austrian German (de-at) (new)
-* Swiss Standard German (de-ch) (new)
-* Simplified Gan Chinese (gan-hans) (new)
-* Traditional Gan Chinese (gan-hant) (new)
-* Literary Chinese (lzh) (new)
-* Uyghur (Latin script) (ug-latn) (renamed from 'ug')
-* Veps (vep) (new)
-* Võro (vro) (renamed from fiu-vro)
-* (bug 17151) Add magic word alias for #redirect for Vietnamese
-* (bug 17288) Messages improved for default language (English)
-* (bug 12937) Update native name for Afar
-* (bug 16909) 'histlegend' now reuses messages instead of copying them
-* (bug 17832) action=delete returns 'unknownerror' instead of 'permissiondenied' 
-  when the user is blocked
-* Traditional/Simplified Gan Chinese conversion support
+* Capiznon (cps) (new)
+* North Frisian (frr) (new)
+* Kirmanjki (kiu) (new)
+* Komi-Permyak (koi) (new)
+* Karachay-Balkar (krc) (new)
+* Latgalian (ltg) (new)
+* Hill Mari (mrj) (new)
+* Prussian (prg) (new)
+* Romagnol (rgn) (new)
+* Rusyn (rue) (new)
+* Lower Silesian (sli) (new)
+* Picard (pcd) (new)
+* Uyghur (Arabic script) (ug-arab) (new)
+* Upper Franconian (vmf) (new)
+* Votic (vot) (new)
+* Eastern Yiddish (ydd) (removed)
+* Iriga Bicolano (bto) (removed)
+* Ladin (lld) (removed)
+* Palembang (plm) (removed)
+* Megleno-Romanian (Greek script) (ruq-grek) (removed)
+* Tamazight (tzm) (removed)
 
-== Compatibility ==
+* (bug 18474) Sorani (ckb - Central Kurdish) (renamed from ku-arab)
+* Add PLURAL function for Scots Gaelic (gd)
+* Add Estonian letters äöõšüž to linktrail (et)
+* (bug 18776) Native name of Burmese language (my)
+* (bug 18806) Use correct unicode characters in spelling of native Chuvash
+  (Чӑвашла)
+* (bug 18864) Updated autonym for Zhuang language
+* (bug 18308) Updated date formatting in Occitan (oc)
+* (bug 19080) Added ăâîşţșțĂÂÎŞŢȘȚ to Romanion (ro) linktrail
+* (bug 19286) Correct commafying function in Polish (pl)
+* (bug 19441) Updated date formatting for Lithuanian
+* (bug 19630) Added ÄäÇçĞğŇňÖöŞşÜüÝýŽž to Turkmen (tk) linktrail
+* (bug 19949) New linktrail for Greek (el)
+* (bug 19809) Korean (North Korea) (ko-kp) (new)
+* (bug 19968) Fixed "Project talk" namespace name for Maltese (mt)
+* (bug 21168) Added áâãàéêçíóôõúü to Portuguese (pt) linktrail
+* (bug 21596) Change interwiki link for Kurdish (ku)
 
-MediaWiki 1.15 requires PHP 5 (5.2 recommended). PHP 4 is no longer supported.
+== Compatibility ==
 
-PHP 5.0.x fails on 64-bit systems due to serious bugs with array processing:
-http://bugs.php.net/bug.php?id=34879
-Upgrade affected systems to PHP 5.1 or higher.
+MediaWiki 1.16 requires PHP 5.1 (5.2 recommended). PHP 4 is no longer supported.
 
 MySQL 3.23.x is no longer supported; some older hosts may need to upgrade.
 At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases.
@@ -463,7 +997,7 @@ At this time we still recommend 4.0, but 4.1/5.0 will work fine in most cases.
 
 == Upgrading ==
 
-1.15 has several database changes since 1.14, and will not work without schema
+1.16 has several database changes since 1.15, and will not work without schema
 updates.
 
 If upgrading from before 1.11, and you are using a wiki as a commons reposito-
@@ -488,7 +1022,7 @@ set $wgMimeType = "application/xhtml+xml"; to test for remaining problem
 cases, but this is not recommended on live sites. (This must be set for
 MathML to display properly in Mozilla.)
 
-For notes on 1.14.x and older releases, see HISTORY.
+For notes on 1.15.x and older releases, see HISTORY.
 
 
 === Online documentation ===
diff --git a/StartProfiler.php b/StartProfiler.php
deleted file mode 100644
index 3fcf69e6..00000000
--- a/StartProfiler.php
+++ /dev/null
@@ -1,22 +0,0 @@
-<?php
-
-require_once( dirname(__FILE__).'/includes/ProfilerStub.php' );
-
-/**
- * To use a profiler, delete the line above and add something like this:
- *
- *   require_once(  dirname(__FILE__).'/includes/Profiler.php' );
- *   $wgProfiler = new Profiler;
- *
- * Or for a sampling profiler:
- *   if ( !mt_rand( 0, 100 ) ) {
- *       require_once(  dirname(__FILE__).'/includes/Profiler.php' );
- *       $wgProfiler = new Profiler;
- *   } else {
- *       require_once(  dirname(__FILE__).'/includes/ProfilerStub.php' );
- *   }
- * 
- * Configuration of the profiler output can be done in LocalSettings.php
- */
-
-
diff --git a/StartProfiler.sample b/StartProfiler.sample
new file mode 100644
index 00000000..f91aeb92
--- /dev/null
+++ b/StartProfiler.sample
@@ -0,0 +1,23 @@
+<?php
+
+require_once( dirname(__FILE__).'/includes/ProfilerStub.php' );
+
+/**
+ * To use a profiler, copy this file to StartProfiler.php,
+ * delete the PHP line above, and add something like this:
+ *
+ *   require_once(  dirname(__FILE__).'/includes/Profiler.php' );
+ *   $wgProfiler = new Profiler;
+ *
+ * Or for a sampling profiler:
+ *   if ( !mt_rand( 0, 100 ) ) {
+ *       require_once(  dirname(__FILE__).'/includes/Profiler.php' );
+ *       $wgProfiler = new Profiler;
+ *   } else {
+ *       require_once(  dirname(__FILE__).'/includes/ProfilerStub.php' );
+ *   }
+ * 
+ * Configuration of the profiler output can be done in LocalSettings.php
+ */
+
+
diff --git a/UPGRADE b/UPGRADE
index 9d0e0521..7cd302e9 100644
--- a/UPGRADE
+++ b/UPGRADE
@@ -42,8 +42,7 @@ You can also obtain the new files directly from our Subversion source code
 repository, via a checkout or export operation.
 
 Replace the existing MediaWiki files with the new. You should preserve the
-LocalSettings.php file, AdminSettings.php file (if present), and the
-"extensions" and "images" directories.
+LocalSettings.php file and the "extensions" and "images" directories.
 
 Depending upon your configuration, you may also need to preserve additional
 directories, including a custom upload directory ($wgUploadDirectory),
@@ -51,8 +50,8 @@ deleted file archives, and any custom skins.
 
 === Perform the database upgrade ===
 
-You will need an AdminSettings.php file set up in the correct format; see
-AdminSettings.sample in the wiki root for more information and examples.
+You will need to have $wgDBadminuser and $wgDBadminpassword set in your
+LocalSettings.php, see there for more info.
 
 From the command line, browse to the "maintenance" directory and run the 
 update.php script to check and update the schema. This will insert missing
@@ -69,8 +68,8 @@ behaviour of MediaWiki.
 
 === Check installed extensions ===
 In MediaWiki 1.14 some extensions are migrated into the core. Please see the
-RELEASE-NOTES section "Migrated extensions" and disable these extensions in your
-localSettings.php
+HISTORY section "Migrated extensions" and disable these extensions in your
+LocalSettings.php
 
 === Test ===
 
@@ -172,10 +171,10 @@ should be replaced with:
 === Web installer ===
 
 You can use the web-based installer wizard if you first remove the
-LocalSettings.php (and AdminSettings.php, if any) files; be sure to
-give the installer the same information as you did on the original
-install (language/encoding, database name, password, etc). This will
-also generate a fresh LocalSettings.php, which you may need to customize.
+LocalSettings.php file; be sure to give the installer the same 
+information as you did on the original install (language/encoding, 
+database name, password, etc). This will also generate a fresh 
+LocalSettings.php, which you may need to customize.
 
 You may change some settings during the install, but be very careful!
 Changing the encoding in particular will generally leave you with a
@@ -185,8 +184,8 @@ lot of corrupt pages, particularly if your wiki is not in English.
 
 Additionally, as of 1.4.0 you can run an in-place upgrade script from
 the command line, keeping your existing LocalSettings.php. This requires
-that you create an AdminSettings.php giving an appropriate database user
-and password with privileges to modify the database structure.
+that you set $wgDBadminuser and $wgDBadminpassword with  an appropriate 
+database user and password with privileges to modify the database structure.
 
 Once the new files are in place, go into the maintenance subdirectory and
 run the script:
diff --git a/api.php b/api.php
index 58e06d88..10baa13e 100644
--- a/api.php
+++ b/api.php
@@ -23,8 +23,8 @@
  * @file
  */
 
-/** 
- * This file is the entry point for all API queries. It begins by checking 
+/**
+ * This file is the entry point for all API queries. It begins by checking
  * whether the API is enabled on this wiki; if not, it informs the user that
  * s/he should set $wgEnableAPI to true and exits. Otherwise, it constructs
  * a new ApiMain using the parameter passed to it as an argument in the URL
@@ -35,9 +35,10 @@
  */
 
 // Initialise common code
-require (dirname(__FILE__) . '/includes/WebStart.php');
+require ( dirname( __FILE__ ) . '/includes/WebStart.php' );
 
-wfProfileIn('api.php');
+wfProfileIn( 'api.php' );
+$starttime = microtime( true );
 
 // URL safety checks
 //
@@ -49,38 +50,67 @@ wfProfileIn('api.php');
 // which will end up triggering HTML detection and execution, hence
 // XSS injection and all that entails.
 //
-// Ensure that all access is through the canonical entry point...
-//
-if( isset( $_SERVER['SCRIPT_URL'] ) ) {
-	$url = $_SERVER['SCRIPT_URL'];
-} else {
-	$url = $_SERVER['PHP_SELF'];
-}
-if( strcmp( "$wgScriptPath/api$wgScriptExtension", $url ) ) {
+if ( $wgRequest->isPathInfoBad() ) {
 	wfHttpError( 403, 'Forbidden',
-		'API must be accessed through the primary script entry point.' );
+		'Invalid file extension found in PATH_INFO. ' .
+		'The API must be accessed through the primary script entry point.' );
 	return;
 }
 
 // Verify that the API has not been disabled
-if (!$wgEnableAPI) {
+if ( !$wgEnableAPI ) {
 	echo 'MediaWiki API is not enabled for this site. Add the following line to your LocalSettings.php';
 	echo '<pre><b>$wgEnableAPI=true;</b></pre>';
-	die(1);
+	die( 1 );
+}
+
+// Selectively allow cross-site AJAX
+
+/*
+ * Helper function to convert wildcard string into a regex
+ * '*' => '.*?'
+ * '?' => '.'
+ * @ return string
+ */
+function convertWildcard( $search ) {
+	$search = preg_quote( $search, '/' );
+	$search = str_replace(
+		array( '\*', '\?' ),
+		array( '.*?', '.' ),
+		$search
+	);
+	return "/$search/";
+}
+
+if ( $wgCrossSiteAJAXdomains && isset( $_SERVER['HTTP_ORIGIN'] ) ) {
+	$exceptions = array_map( 'convertWildcard', $wgCrossSiteAJAXdomainExceptions );
+	$regexes = array_map( 'convertWildcard', $wgCrossSiteAJAXdomains );
+	foreach ( $regexes as $regex ) {
+		if ( preg_match( $regex, $_SERVER['HTTP_ORIGIN'] ) ) {
+			foreach ( $exceptions as $exc ) { // Check against exceptions
+				if ( preg_match( $exc, $_SERVER['HTTP_ORIGIN'] ) ) {
+					break 2;
+				}
+			}
+			header( "Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}" );
+			header( 'Access-Control-Allow-Credentials: true' );
+			break;
+		}
+	}
 }
 
 // So extensions can check whether they're running in API mode
-define('MW_API', true);
+define( 'MW_API', true );
 
 // Set a dummy $wgTitle, because $wgTitle == null breaks various things
 // In a perfect world this wouldn't be necessary
-$wgTitle = Title::newFromText('API');
+$wgTitle = Title::makeTitle( NS_MAIN, 'API' );
 
 /* Construct an ApiMain with the arguments passed via the URL. What we get back
  * is some form of an ApiMain, possibly even one that produces an error message,
  * but we don't care here, as that is handled by the ctor.
  */
-$processor = new ApiMain($wgRequest, $wgEnableWriteAPI);
+$processor = new ApiMain( $wgRequest, $wgEnableWriteAPI );
 
 // Process data & print results
 $processor->execute();
@@ -89,9 +119,28 @@ $processor->execute();
 wfDoUpdates();
 
 // Log what the user did, for book-keeping purposes.
-wfProfileOut('api.php');
+$endtime = microtime( true );
+wfProfileOut( 'api.php' );
 wfLogProfilingData();
 
+// Log the request
+if ( $wgAPIRequestLog ) {
+	$items = array(
+			wfTimestamp( TS_MW ),
+			$endtime - $starttime,
+			wfGetIP(),
+			$_SERVER['HTTP_USER_AGENT']
+	);
+	$items[] = $wgRequest->wasPosted() ? 'POST' : 'GET';
+	if ( $processor->getModule()->mustBePosted() ) {
+		$items[] = "action=" . $wgRequest->getVal( 'action' );
+	} else {
+		$items[] = wfArrayToCGI( $wgRequest->getValues() );
+	}
+	wfErrorLog( implode( ',', $items ) . "\n", $wgAPIRequestLog );
+	wfDebug( "Logged API request to $wgAPIRequestLog\n" );
+}
+
 // Shut down the database
 wfGetLBFactory()->shutdown();
 
diff --git a/cache/.htaccess b/cache/.htaccess
new file mode 100644
index 00000000..3a428827
--- /dev/null
+++ b/cache/.htaccess
@@ -0,0 +1 @@
+Deny from all
diff --git a/config/Installer.php b/config/Installer.php
new file mode 100644
index 00000000..293a1a6c
--- /dev/null
+++ b/config/Installer.php
@@ -0,0 +1,2350 @@
+<?php
+
+# MediaWiki web-based config/installation
+# Copyright (C) 2004 Brion Vibber <brion@pobox.com>, 2006 Rob Church <robchur@gmail.com>
+# http://www.mediawiki.org/
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+# http://www.gnu.org/copyleft/gpl.html
+
+if( !defined( 'MEDIAWIKI_INSTALL' ) ) {
+	die( 'Not an entry point.' );
+}
+
+error_reporting( E_ALL | E_STRICT );
+header( "Content-type: text/html; charset=utf-8" );
+@ini_set( "display_errors", true );
+
+# In case of errors, let output be clean.
+$wgRequestTime = microtime( true );
+
+// Run version checks before including other files
+// so people don't see a scary parse error.
+require_once( "$IP/maintenance/install-utils.inc" );
+install_version_checks();
+
+require_once( "$IP/includes/Defines.php" );
+require_once( "$IP/includes/DefaultSettings.php" );
+require_once( "$IP/includes/AutoLoader.php" );
+require_once( "$IP/includes/MagicWord.php" );
+require_once( "$IP/includes/Namespace.php" );
+require_once( "$IP/includes/ProfilerStub.php" );
+require_once( "$IP/includes/GlobalFunctions.php" );
+require_once( "$IP/includes/Hooks.php" );
+require_once( "$IP/includes/Exception.php" );
+require_once( "$IP/includes/json/Services_JSON.php" );
+require_once( "$IP/includes/json/FormatJson.php" );
+
+# If we get an exception, the user needs to know
+# all the details
+$wgShowExceptionDetails = true;
+$wgShowSQLErrors = true;
+wfInstallExceptionHandler();
+## Databases we support:
+
+$ourdb = array();
+
+$ourdb['mysql'] = array(
+	'fullname'   => 'MySQL',
+	'havedriver' => 0,
+	'compile'    => 'mysql',
+	'bgcolor'    => '#ffe5a7',
+	'rootuser'   => 'root',
+	'serverless' => false
+);
+
+$ourdb['postgres'] = array(
+	'fullname'   => 'PostgreSQL',
+	'havedriver' => 0,
+	'compile'    => 'pgsql',
+	'bgcolor'    => '#aaccff',
+	'rootuser'   => 'postgres',
+	'serverless' => false
+);
+
+$ourdb['sqlite'] = array(
+	'fullname'   => 'SQLite',
+	'havedriver' => 0,
+	'compile'    => 'pdo_sqlite',
+	'bgcolor'    => '#b1ebb1',
+	'rootuser'   => '',
+	'serverless' =>  true
+);
+
+$ourdb['mssql'] = array(
+	'fullname'   => 'MSSQL',
+	'havedriver' => 0,
+	'compile'    => 'mssql_not_ready', # Change to 'mssql' after includes/DatabaseMssql.php added;
+	'bgcolor'    => '#ffc0cb',
+	'rootuser'   => 'administrator',
+	'serverless' => false
+);
+
+$ourdb['ibm_db2'] = array(
+	'fullname'   => 'DB2',
+	'havedriver' => 0,
+	'compile'    => 'ibm_db2',
+	'bgcolor'    => '#ffeba1',
+	'rootuser'   => 'db2admin',
+	'serverless' => false
+);
+
+$ourdb['oracle'] = array(
+	'fullname'   => 'Oracle',
+	'havedriver' => 0,
+	'compile'    => 'oci8',
+	'bgcolor'    => '#ffeba1',
+	'rootuser'   => 'sys',
+	'serverless' => false
+);
+
+?>
+<!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" lang="en" dir="ltr">
+<head>
+	<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+	<meta name="robots" content="noindex,nofollow"/>
+	<title>MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?> Installation
+	
+	
+
+
+
+
+
+
+
+ +

MediaWiki Installation

+ +Setup has completed, your wiki is configured.

+

Please delete the /config directory for extra security.

" ); +} + +if( file_exists( "./LocalSettings.php" ) ) { + writeSuccessMessage(); + dieout( '' ); +} + +if( !is_writable( "." ) ) { + dieout( "

Can't write config file, aborting

+ +

In order to configure the wiki you have to make the config subdirectory + writable by the web server. Once configuration is done you'll move the created + LocalSettings.php to the parent directory, and for added safety you can + then remove the config subdirectory entirely.

+ +

To make the directory writable on a Unix/Linux system:

+ +
+	cd " . htmlspecialchars( dirname( dirname( __FILE__ ) ) ) . "
+	chmod a+w config
+	
+ +

Afterwards retry to start the setup.

" ); +} + + +require_once( "$IP/maintenance/updaters.inc" ); + +class ConfigData { + function getEncoded( $data ) { + # removing latin1 support, no need... + return $data; + } + function getSitename() { return $this->getEncoded( $this->Sitename ); } + function getSysopName() { return $this->getEncoded( $this->SysopName ); } + function getSysopPass() { return $this->getEncoded( $this->SysopPass ); } + + function setSchema( $schema, $engine ) { + $this->DBschema = $schema; + if ( !preg_match( '/^\w*$/', $engine ) ){ + $engine = 'InnoDB'; + } + switch ( $this->DBschema ) { + case 'mysql5': + $this->DBTableOptions = "ENGINE=$engine, DEFAULT CHARSET=utf8"; + $this->DBmysql5 = 'true'; + break; + case 'mysql5-binary': + $this->DBTableOptions = "ENGINE=$engine, DEFAULT CHARSET=binary"; + $this->DBmysql5 = 'true'; + break; + default: + $this->DBTableOptions = "TYPE=$engine"; + $this->DBmysql5 = 'false'; + } + $this->DBengine = $engine; + + # Set the global for use during install + global $wgDBTableOptions; + $wgDBTableOptions = $this->DBTableOptions; + } +} + +?> + + + + +

Checking environment...

+

Please include all of the lines below when reporting installation problems.

+
    +PHP " . htmlspecialchars( phpversion() ) . " installed\n"; + +error_reporting( 0 ); +$phpdatabases = array(); +foreach (array_keys($ourdb) as $db) { + $compname = $ourdb[$db]['compile']; + if( extension_loaded( $compname ) || ( mw_have_dl() && dl( "{$compname}." . PHP_SHLIB_SUFFIX ) ) ) { + array_push($phpdatabases, $db); + $ourdb[$db]['havedriver'] = 1; + } +} +error_reporting( E_ALL | E_STRICT ); + +if (!$phpdatabases) { + print "Could not find a suitable database driver!
      "; + foreach (array_keys($ourdb) AS $db) { + $comp = $ourdb[$db]['compile']; + $full = $ourdb[$db]['fullname']; + print "
    • For $full, compile PHP using --with-$comp, " + ."or install the $comp.so module
    • \n"; + } + echo '
    '; + dieout( '' ); +} + +print "
  • Found database drivers for:"; +$DefaultDBtype = ''; +foreach (array_keys($ourdb) AS $db) { + if ($ourdb[$db]['havedriver']) { + if ( $DefaultDBtype == '' ) { + $DefaultDBtype = $db; + } + print " ".$ourdb[$db]['fullname']; + } +} +print "
  • \n"; + +if( wfIniGetBool( "register_globals" ) ) { + ?> +
  • +
    + Warning: + PHP's register_globals option is enabled. Disable it if you can. +
    + MediaWiki will work, but your server is more exposed to PHP-based security vulnerabilities. +
  • +
  • Fatal: magic_quotes_runtime is active! + This option corrupts data input unpredictably; you cannot install or use + MediaWiki unless this option is disabled.
  • +
  • Fatal: magic_quotes_sybase is active! + This option corrupts data input unpredictably; you cannot install or use + MediaWiki unless this option is disabled.
  • +
  • Fatal: mbstring.func_overload is active! + This option causes errors and may corrupt data unpredictably; + you cannot install or use MediaWiki unless this option is disabled.
  • +
  • Fatal: zend.ze1_compatibility_mode is active! + This option causes horrible bugs with MediaWiki; you cannot install or use + MediaWiki unless this option is disabled.
  • + safeMode = true; + ?> +
  • Warning: PHP's + safe mode is active. + You may have problems caused by this, particularly if using image uploads. +
  • + safeMode = false; +} + +$sapi = htmlspecialchars( php_sapi_name() ); +print "
  • PHP server API is $sapi; "; +$script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php'; +if( $wgUsePathInfo ) { + print "ok, using pretty URLs ($script/Page_Title)"; +} else { + print "using ugly URLs ($script?title=Page_Title)"; +} +print "
  • \n"; + +$conf->xml = function_exists( "utf8_encode" ); +if( $conf->xml ) { + print "
  • Have XML / Latin1-UTF-8 conversion support.
  • \n"; +} else { + dieout( "PHP's XML module is missing; the wiki requires functions in + this module and won't work in this configuration. + If you're running Mandrake, install the php-xml package." ); +} + +# Check for session support +if( !function_exists( 'session_name' ) ) + dieout( "PHP's session module is missing. MediaWiki requires session support in order to function." ); + +# session.save_path doesn't *have* to be set, but if it is, and it's +# not valid/writable/etc. then it can cause problems +$sessionSavePath = mw_get_session_save_path(); +$ssp = htmlspecialchars( $sessionSavePath ); +# Warn the user if it's not set, but let them proceed +if( !$sessionSavePath ) { + print "
  • Warning: A value for session.save_path + has not been set in PHP.ini. If the default value causes problems with + saving session data, set it to a valid path which is read/write/execute + for the user your web server is running under.
  • "; +} elseif ( is_dir( $sessionSavePath ) && is_writable( $sessionSavePath ) ) { + # All good? Let the user know + print "
  • Session save path ({$ssp}) appears to be valid.
  • "; +} else { + # Something not right? Warn the user, but let them proceed + print "
  • Warning: Your session.save_path value ({$ssp}) + appears to be invalid or is not writable. PHP needs to be able to save data to + this location for correct session operation.
  • "; +} + +# Check for PCRE support +if( !function_exists( 'preg_match' ) ) + dieout( "The PCRE support module appears to be missing. MediaWiki requires the + Perl-compatible regular expression functions." ); + +# The installer can take a while, and we really don't want it to time out +wfSuppressWarnings(); +set_time_limit( 0 ); +wfRestoreWarnings(); + +$memlimit = ini_get( "memory_limit" ); +if( $memlimit == -1 ) { + print "
  • PHP is configured with no memory_limit.
  • \n"; +} else { + print "
  • PHP's memory_limit is " . htmlspecialchars( $memlimit ). " bytes. "; + $newlimit = wfMemoryLimit(); + $memlimit = wfShorthandToInteger( $memlimit ); + if( $newlimit < $memlimit ) { + print "Failed raising limit, installation may fail."; + } elseif ( $newlimit > $memlimit ) { + print "Raised memory_limit to " . htmlspecialchars( $newlimit ) . " bytes. "; + } + print "
  • \n"; +} + +$conf->xcache = function_exists( 'xcache_get' ); +if( $conf->xcache ) + print "
  • XCache installed
  • \n"; + +$conf->apc = function_exists('apc_fetch'); +if ($conf->apc ) { + print "
  • APC installed
  • \n"; +} + +$conf->eaccel = function_exists( 'eaccelerator_get' ); +if ( $conf->eaccel ) { + print "
  • eAccelerator installed
  • \n"; +} + +$conf->dba = function_exists( 'dba_open' ); + +if( !( $conf->eaccel || $conf->apc || $conf->xcache ) ) { + echo( '
  • Couldn\'t find eAccelerator, + APC or XCache; + cannot use these for object caching.
  • ' ); +} + +$conf->diff3 = false; +$diff3locations = array_merge( + array( + "/usr/bin", + "/usr/local/bin", + "/opt/csw/bin", + "/usr/gnu/bin", + "/usr/sfw/bin" ), + explode( PATH_SEPARATOR, getenv( "PATH" ) ) ); +$diff3names = array( "gdiff3", "diff3", "diff3.exe" ); + +$diff3versioninfo = array( '$1 --version 2>&1', 'diff3 (GNU diffutils)' ); +foreach ($diff3locations as $loc) { + $exe = locate_executable($loc, $diff3names, $diff3versioninfo); + if ($exe !== false) { + $conf->diff3 = $exe; + break; + } +} + +if ($conf->diff3) + print "
  • Found GNU diff3: $conf->diff3.
  • "; +else + print "
  • GNU diff3 not found.
  • "; + +$conf->ImageMagick = false; +$imcheck = array( "/usr/bin", "/opt/csw/bin", "/usr/local/bin", "/sw/bin", "/opt/local/bin" ); +foreach( $imcheck as $dir ) { + $im = "$dir/convert"; + if( @file_exists( $im ) ) { + print "
  • Found ImageMagick: $im; image thumbnailing will be enabled if you enable uploads.
  • \n"; + $conf->ImageMagick = $im; + break; + } +} + +$conf->HaveGD = function_exists( "imagejpeg" ); +if( $conf->HaveGD ) { + print "
  • Found GD graphics library built-in"; + if( !$conf->ImageMagick ) { + print ", image thumbnailing will be enabled if you enable uploads"; + } + print ".
  • \n"; +} else { + if( !$conf->ImageMagick ) { + print "
  • Couldn't find GD library or ImageMagick; image thumbnailing disabled.
  • \n"; + } +} + +$conf->IP = dirname( dirname( __FILE__ ) ); +print "
  • Installation directory: " . htmlspecialchars( $conf->IP ) . "
  • \n"; + + +// PHP_SELF isn't available sometimes, such as when PHP is CGI but +// cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME +// to get the path to the current script... hopefully it's reliable. SIGH +$path = ($_SERVER["PHP_SELF"] === '') + ? $_SERVER["SCRIPT_NAME"] + : $_SERVER["PHP_SELF"]; + +$conf->ScriptPath = preg_replace( '{^(.*)/config.*$}', '$1', $path ); +print "
  • Script URI path: " . htmlspecialchars( $conf->ScriptPath ) . "
  • \n"; + + + +// We may be installing from *.php5 extension file, if so, print message +$conf->ScriptExtension = '.php'; +if (defined('MW_INSTALL_PHP5_EXT')) { + $conf->ScriptExtension = '.php5'; + print "
  • Installing MediaWiki with php5 file extensions
  • \n"; +} else { + print "
  • Installing MediaWiki with php file extensions
  • \n"; +} + + +print "
  • Environment checked. You can install MediaWiki.
  • \n"; + $conf->posted = ($_SERVER["REQUEST_METHOD"] == "POST"); + + $conf->Sitename = ucfirst( importPost( "Sitename", "" ) ); + $defaultEmail = empty( $_SERVER["SERVER_ADMIN"] ) + ? 'root@localhost' + : $_SERVER["SERVER_ADMIN"]; + $conf->EmergencyContact = importPost( "EmergencyContact", $defaultEmail ); + $conf->DBtype = importPost( "DBtype", $DefaultDBtype ); + if ( !isset( $ourdb[$conf->DBtype] ) ) { + $conf->DBtype = $DefaultDBtype; + } + + $conf->DBserver = importPost( "DBserver", "localhost" ); + $conf->DBname = importPost( "DBname", "wikidb" ); + $conf->DBuser = importPost( "DBuser", "wikiuser" ); + $conf->DBpassword = importPost( "DBpassword" ); + $conf->DBpassword2 = importPost( "DBpassword2" ); + $conf->SysopName = importPost( "SysopName", "WikiSysop" ); + $conf->SysopPass = importPost( "SysopPass" ); + $conf->SysopPass2 = importPost( "SysopPass2" ); + $conf->RootUser = importPost( "RootUser" ); + $conf->RootPW = importPost( "RootPW", "" ); + $useRoot = importCheck( 'useroot', false ); + $conf->LanguageCode = importPost( "LanguageCode", "en" ); + ## MySQL specific: + $conf->DBprefix = importPost( "DBprefix" ); + $conf->setSchema( + importPost( "DBschema", "mysql5-binary" ), + importPost( "DBengine", "InnoDB" ) ); + + ## Postgres specific: + $conf->DBport = importPost( "DBport", "5432" ); + $conf->DBts2schema = importPost( "DBts2schema", "public" ); + $conf->DBpgschema = importPost( "DBpgschema", "mediawiki" ); + + ## SQLite specific + $conf->SQLiteDataDir = importPost( "SQLiteDataDir", "$IP/../data" ); + + ## MSSQL specific + // We need a second field so it doesn't overwrite the MySQL one + $conf->DBprefix2 = importPost( "DBprefix2" ); + + ## DB2 specific: + // New variable in order to have a different default port number + $conf->DBport_db2 = importPost( "DBport_db2", "50000" ); + $conf->DBcataloged = importPost( "DBcataloged", "cataloged" ); + $conf->DBdb2schema = importPost( "DBdb2schema", "mediawiki" ); + + // Oracle specific + $conf->DBprefix_ora = importPost( "DBprefix_ora" ); + $conf->DBdefTS_ora = importPost( "DBdefTS_ora", "USERS" ); + $conf->DBtempTS_ora = importPost( "DBtempTS_ora", "TEMP" ); + + $conf->ShellLocale = getShellLocale( $conf->LanguageCode ); + +/* Check for validity */ +$errs = array(); + +if( preg_match( '/^$|^mediawiki$|#/i', $conf->Sitename ) ) { + $errs["Sitename"] = "Must not be blank or \"MediaWiki\" and may not contain \"#\""; +} +if( !$ourdb[$conf->DBtype]['serverless'] ) { + if( $conf->DBuser == "" ) { + $errs["DBuser"] = "Must not be blank"; + } + if( ($conf->DBtype == 'mysql') && (strlen($conf->DBuser) > 16) ) { + $errs["DBuser"] = "Username too long"; + } + if( $conf->DBpassword == "" && $conf->DBtype != "postgres" ) { + $errs["DBpassword"] = "Must not be blank"; + } + if( $conf->DBpassword != $conf->DBpassword2 ) { + $errs["DBpassword2"] = "Passwords don't match!"; + } +} +if( !preg_match( '/^[A-Za-z_0-9]*$/', $conf->DBprefix ) ) { + $errs["DBprefix"] = "Invalid table prefix"; +} else { + untaint( $conf->DBprefix, TC_MYSQL ); +} +if( !preg_match( '/^[A-Za-z_0-9]*$/', $conf->DBprefix_ora ) ) { + $errs["DBprefix_ora"] = "Invalid table prefix"; +} + +error_reporting( E_ALL | E_STRICT ); + +/** + * Initialise $wgLang and $wgContLang to something so we can + * call case-folding methods. Per Brion, this is English for + * now, although we could be clever and initialise to the + * user-selected language. + */ +$wgContLang = Language::factory( 'en' ); +$wgLang = $wgContLang; + +/** + * We're messing about with users, so we need a stub + * authentication plugin... + */ +$wgAuth = new AuthPlugin(); + +/** + * Validate the initial administrator account; username, + * password checks, etc. + */ +if( $conf->SysopName ) { + # Check that the user can be created + $u = User::newFromName( $conf->SysopName ); + if( $u instanceof User ) { + # Various password checks + if( $conf->SysopPass != '' ) { + if( $conf->SysopPass == $conf->SysopPass2 ) { + if( !$u->isValidPassword( $conf->SysopPass ) ) { + $errs['SysopPass'] = "Bad password"; + } + } else { + $errs['SysopPass2'] = "Passwords don't match"; + } + } else { + $errs['SysopPass'] = "Cannot be blank"; + } + unset( $u ); + } else { + $errs['SysopName'] = "Bad username"; + } +} + +$conf->License = importRequest( "License", "none" ); +if( $conf->License == "gfdl1_2" ) { + $conf->RightsUrl = "http://www.gnu.org/licenses/old-licenses/fdl-1.2.txt"; + $conf->RightsText = "GNU Free Documentation License 1.2"; + $conf->RightsCode = "gfdl1_2"; + $conf->RightsIcon = '${wgScriptPath}/skins/common/images/gnu-fdl.png'; +} elseif( $conf->License == "gfdl1_3" ) { + $conf->RightsUrl = "http://www.gnu.org/copyleft/fdl.html"; + $conf->RightsText = "GNU Free Documentation License 1.3"; + $conf->RightsCode = "gfdl1_3"; + $conf->RightsIcon = '${wgScriptPath}/skins/common/images/gnu-fdl.png'; +} elseif( $conf->License == "none" ) { + $conf->RightsUrl = $conf->RightsText = $conf->RightsCode = $conf->RightsIcon = ""; +} elseif( $conf->License == "pd" ) { + $conf->RightsUrl = "http://creativecommons.org/licenses/publicdomain/"; + $conf->RightsText = "Public Domain"; + $conf->RightsCode = "pd"; + $conf->RightsIcon = '${wgScriptPath}/skins/common/images/public-domain.png'; +} else { + $conf->RightsUrl = importRequest( "RightsUrl", "" ); + $conf->RightsText = importRequest( "RightsText", "" ); + $conf->RightsCode = importRequest( "RightsCode", "" ); + $conf->RightsIcon = importRequest( "RightsIcon", "" ); +} + +$conf->Shm = importRequest( "Shm", "none" ); +$conf->MCServers = importRequest( "MCServers" ); + +/* Test memcached servers */ + +if ( $conf->Shm == 'memcached' && $conf->MCServers ) { + $conf->MCServerArray = wfArrayMap( 'trim', explode( ',', $conf->MCServers ) ); + foreach ( $conf->MCServerArray as $server ) { + $error = testMemcachedServer( $server ); + if ( $error ) { + $errs["MCServers"] = $error; + break; + } + } +} else if ( $conf->Shm == 'memcached' ) { + $errs["MCServers"] = "Please specify at least one server if you wish to use memcached"; +} + +/* default values for installation */ +$conf->Email = importRequest("Email", "email_enabled"); +$conf->Emailuser = importRequest("Emailuser", "emailuser_enabled"); +$conf->Enotif = importRequest("Enotif", "enotif_allpages"); +$conf->Eauthent = importRequest("Eauthent", "eauthent_enabled"); + +if( $conf->posted && ( 0 == count( $errs ) ) ) { + do { /* So we can 'continue' to end prematurely */ + $conf->Root = ($conf->RootPW != ""); + + /* Load up the settings and get installin' */ + $local = writeLocalSettings( $conf ); + echo "
  • \n"; + echo "

    Generating configuration file...

    \n"; + echo "
  • \n"; + + $wgCommandLineMode = false; + chdir( ".." ); + $ok = eval( $local ); + if( $ok === false ) { + dieout( "

    Errors in generated configuration; " . + "most likely due to a bug in the installer... " . + "Config file was:

    " . + "
    " .
    +				htmlspecialchars( $local ) .
    +				"
    " ); + } + $conf->DBtypename = ''; + foreach (array_keys($ourdb) as $db) { + if ($conf->DBtype === $db) + $conf->DBtypename = $ourdb[$db]['fullname']; + } + if ( ! strlen($conf->DBtype)) { + $errs["DBpicktype"] = "Please choose a database type"; + continue; + } + + if (! $conf->DBtypename) { + $errs["DBtype"] = "Unknown database type '$conf->DBtype'"; + continue; + } + print "
  • Database type: " . htmlspecialchars( $conf->DBtypename ) . "
  • \n"; + $dbclass = 'Database'.ucfirst($conf->DBtype); + $wgDBtype = $conf->DBtype; + $wgDBadminuser = "root"; + $wgDBadminpassword = $conf->RootPW; + + ## Mysql specific: + $wgDBprefix = $conf->DBprefix; + + ## Postgres specific: + $wgDBport = $conf->DBport; + $wgDBts2schema = $conf->DBts2schema; + + if( $wgDBtype == 'postgres' ) { + $wgDBmwschema = $conf->DBpgschema; + } elseif ( $wgDBtype == 'ibm_db2' ) { + $wgDBmwschema = $conf->DBdb2schema; + } + + if( $conf->DBprefix2 != '' ) { + // For MSSQL + $wgDBprefix = $conf->DBprefix2; + } elseif( $conf->DBprefix_ora != '' ) { + // For Oracle + $wgDBprefix = $conf->DBprefix_ora; + } + + ## DB2 specific: + $wgDBcataloged = $conf->DBcataloged; + + $wgCommandLineMode = true; + if (! defined ( 'STDERR' ) ) + define( 'STDERR', fopen("php://stderr", "wb")); + $wgUseDatabaseMessages = false; /* FIXME: For database failure */ + require_once( "$IP/includes/Setup.php" ); + Language::getLocalisationCache()->disableBackend(); + + chdir( "config" ); + + $wgTitle = Title::newFromText( "Installation script" ); + error_reporting( E_ALL | E_STRICT ); + print "
  • Loading class: " . htmlspecialchars( $dbclass ) . "
  • \n"; + if ( $conf->DBtype != 'sqlite' ) { + $dbc = new $dbclass; + } + + if( $conf->DBtype == 'mysql' ) { + $mysqlOldClient = version_compare( mysql_get_client_info(), "4.1.0", "lt" ); + if( $mysqlOldClient ) { + print "
  • PHP is linked with old MySQL client libraries. If you are + using a MySQL 4.1 server and have problems connecting to the database, + see http://dev.mysql.com/doc/mysql/en/old-client.html for help.
  • \n"; + } + $ok = true; # Let's be optimistic + + # Decide if we're going to use the superuser or the regular database user + $conf->Root = $useRoot; + if( $conf->Root ) { + $db_user = $conf->RootUser; + $db_pass = $conf->RootPW; + } else { + $db_user = $wgDBuser; + $db_pass = $wgDBpassword; + } + + # Attempt to connect + echo( "
  • Attempting to connect to database server as " . htmlspecialchars( $db_user ) . "..." ); + $wgDatabase = Database::newFromParams( $wgDBserver, $db_user, $db_pass, '', 1 ); + + # Check the connection and respond to errors + if( $wgDatabase->isOpen() ) { + # Seems OK + $ok = true; + $wgDBadminuser = $db_user; + $wgDBadminpassword = $db_pass; + echo( "success.
  • \n" ); + $wgDatabase->ignoreErrors( true ); + $myver = $wgDatabase->getServerVersion(); + } else { + # There were errors, report them and back out + $ok = false; + $errno = mysql_errno(); + $errtx = htmlspecialchars( mysql_error() ); + switch( $errno ) { + case 1045: + case 2000: + echo( "failed due to authentication errors. Check passwords." ); + if( $conf->Root ) { + # The superuser details are wrong + $errs["RootUser"] = "Check username"; + $errs["RootPW"] = "and password"; + } else { + # The regular user details are wrong + $errs["DBuser"] = "Check username"; + $errs["DBpassword"] = "and password"; + } + break; + case 2002: + case 2003: + default: + # General connection problem + echo( htmlspecialchars( "failed with error [$errno] $errtx." ) . "\n" ); + $errs["DBserver"] = "Connection failed"; + break; + } # switch + } #conn. att. + + if( !$ok ) { continue; } + } + else if( $conf->DBtype == 'ibm_db2' ) { + if( $useRoot ) { + $db_user = $conf->RootUser; + $db_pass = $conf->RootPW; + } else { + $db_user = $wgDBuser; + $db_pass = $wgDBpassword; + } + + echo( "
  • Attempting to connect to database \"" . htmlspecialchars( $wgDBname ) . + "\" as \"" . htmlspecialchars( $db_user ) . "\"..." ); + $wgDatabase = $dbc->newFromParams($wgDBserver, $db_user, $db_pass, $wgDBname, 1); + // enable extra debug messages + $dbc->setMode(DatabaseIbm_db2::INSTALL_MODE); + $wgDatabase->setMode(DatabaseIbm_db2::INSTALL_MODE); + + if (!$wgDatabase->isOpen()) { + print " error: " . htmlspecialchars( $wgDatabase->lastError() ) . "
  • \n"; + } else { + $myver = $wgDatabase->getServerVersion(); + } + if (is_callable(array($wgDatabase, 'initial_setup'))) $wgDatabase->initial_setup('', $wgDBname); + + } elseif ( $conf->DBtype == 'sqlite' ) { + $wgSQLiteDataDir = $conf->SQLiteDataDir; + echo '
  • Attempting to connect to SQLite database at "' . + htmlspecialchars( $wgSQLiteDataDir ) . '": '; + if ( !is_dir( $wgSQLiteDataDir ) ) { + if ( is_writable( dirname( $wgSQLiteDataDir ) ) ) { + $ok = wfMkdirParents( $wgSQLiteDataDir, $wgSQLiteDataDirMode ); + } else { + $ok = false; + } + if ( !$ok ) { + echo "cannot create data directory
  • "; + $errs['SQLiteDataDir'] = 'Enter a valid data directory'; + continue; + } + } + if ( !is_writable( $wgSQLiteDataDir ) ) { + echo "data directory not writable"; + $errs['SQLiteDataDir'] = 'Enter a writable data directory'; + continue; + } + $dataFile = DatabaseSqlite::generateFileName( $wgSQLiteDataDir, $wgDBname ); + if ( file_exists( $dataFile ) ) { + if ( !is_writable( $dataFile ) ) { + echo "data file not writable"; + $errs['SQLiteDataDir'] = basename( $dataFile ) . " is not writable"; + continue; + } + } else { + if ( file_put_contents( $dataFile, '' ) === false ) { + echo 'could not create database file "' . htmlspecialchars( basename( $dataFile ) ) . "\"\n"; + $errs['SQLiteDataDir'] = "couldn't create " . basename( $dataFile ); + continue; + } + } + try { + $wgDatabase = new DatabaseSqlite( false, false, false, $wgDBname, 1 ); + } + catch( MWException $ex ) { + echo 'error: ' . htmlspecialchars( $ex->getMessage() ) . "\n"; + continue; + } + + if (!$wgDatabase->isOpen()) { + print "error: " . htmlspecialchars( $wgDatabase->lastError() ) . "\n"; + $errs['SQLiteDataDir'] = 'Could not connect to database'; + continue; + } else { + $myver = $wgDatabase->getServerVersion(); + } + if ( is_callable( array( $wgDatabase, 'initial_setup' ) ) ) { + $wgDatabase->initial_setup('', $wgDBname); + } + echo "ok\n"; + } elseif ( $conf->DBtype == 'oracle' ) { + echo "
  • Attempting to connect to database \"" . htmlspecialchars( $wgDBname ) ."\"
  • "; + $old_error_level = error_reporting(); + wfSuppressWarnings(); + $wgDatabase = $dbc->newFromParams('DUMMY', $wgDBuser, $wgDBpassword, $wgDBname, 1); + wfRestoreWarnings(); + if (!$wgDatabase->isOpen()) { + $ok = true; + echo "
  • Connect failed.
  • "; + if ($useRoot) { + if (ini_get('oci8.privileged_connect') === false) { + echo "
  • Privileged connect disabled, please set oci8.privileged_connect or run maintenance/ora/user.sql script manually prior to continuing.
  • "; + $ok = false; + } else { + $wgDBadminuser = $conf->RootUser; + $wgDBadminpassword = $conf->RootPW; + echo "
  • Attempting to create DB user.
  • "; + $wgDatabase = $dbc->newFromParams('DUMMY', $wgDBadminuser, $wgDBadminpassword, $wgDBname, 1, 64); + if ($wgDatabase->isOpen()) { + $wgDBOracleDefTS = $conf->DBdefTS_ora; + $wgDBOracleTempTS = $conf->DBtempTS_ora; + $res = $wgDatabase->sourceFile( "../maintenance/ora/user.sql" ); + if ($res !== true) dieout($res); + } else { + echo "
  • Invalid database superuser, please supply a valid superuser account.
  • "; + echo "
  • ERR: ".print_r(oci_error(), true)."
  • "; + $ok = false; + } + } + } else { + echo "
  • Database superuser missing, please supply a valid superuser account.
  • "; + $ok = false; + } + if (!$ok) { + $errs["RootUser"] = "Check username"; + $errs["RootPW"] = "and password"; + } else { + echo "
  • Attempting to connect to database with new user \"" . htmlspecialchars( $wgDBname ) ."\"
  • "; + $wgDatabase = $dbc->newFromParams('DUMMY', $wgDBuser, $wgDBpassword, $wgDBname, 1); + } + } + if ($ok) { + $myver = $wgDatabase->getServerVersion(); + } + } else { # not mysql + error_reporting( E_ALL | E_STRICT ); + $wgSuperUser = ''; + ## Possible connect as a superuser + // Changed !mysql to postgres check since it seems to only apply to postgres + if( $useRoot && $conf->DBtype == 'postgres' ) { + $wgDBsuperuser = $conf->RootUser; + echo( "
  • Attempting to connect to database \"postgres\" as superuser \"" . + htmlspecialchars( $wgDBsuperuser ) . "\"..." ); + $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBsuperuser, $conf->RootPW, "postgres", 1); + if (!$wgDatabase->isOpen()) { + print " error: " . htmlspecialchars( $wgDatabase->lastError() ) . "
  • \n"; + $errs["DBserver"] = "Could not connect to database as superuser"; + $errs["RootUser"] = "Check username"; + $errs["RootPW"] = "and password"; + continue; + } + $wgDatabase->initial_setup($conf->RootPW, 'postgres'); + } + echo( "
  • Attempting to connect to database \"" . htmlspecialchars( $wgDBname ) . + "\" as \"" . htmlspecialchars( $wgDBuser ) . "\"..." ); + $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1); + if (!$wgDatabase->isOpen()) { + print " error: " . htmlspecialchars( $wgDatabase->lastError() ) . "
  • \n"; + $errs["DBserver"] = "Could not connect to database as user"; + $errs["DBuser"] = "Check username"; + $errs["DBpassword"] = "and password"; + continue; + } else { + $myver = $wgDatabase->getServerVersion(); + } + if (is_callable(array($wgDatabase, 'initial_setup'))) $wgDatabase->initial_setup('', $wgDBname); + } + + if ( !$wgDatabase->isOpen() ) { + $errs["DBserver"] = "Couldn't connect to database"; + continue; + } + + print "
  • Connected to " . htmlspecialchars( "{$conf->DBtype} $myver" ); + if ($conf->DBtype == 'mysql') { + if( version_compare( $myver, "4.0.14" ) < 0 ) { + print "
  • \n"; + dieout( "-- mysql 4.0.14 or later required. Aborting." ); + } + $mysqlNewAuth = version_compare( $myver, "4.1.0", "ge" ); + if( $mysqlNewAuth && $mysqlOldClient ) { + print "; You are using MySQL 4.1 server, but PHP is linked + to old client libraries; if you have trouble with authentication, see + http://dev.mysql.com/doc/mysql/en/old-client.html for help."; + } + if( $wgDBmysql5 ) { + if( $mysqlNewAuth ) { + print "; enabling MySQL 4.1/5.0 charset mode"; + } else { + print "; MySQL 4.1/5.0 charset mode enabled, + but older version detected; will likely fail."; + } + } + print "\n"; + + @$sel = $wgDatabase->selectDB( $wgDBname ); + if( $sel ) { + print "
  • Database " . htmlspecialchars( $wgDBname ) . " exists
  • \n"; + } else { + $err = mysql_errno(); + $databaseSafe = htmlspecialchars( $wgDBname ); + if( $err == 1102 /* Invalid database name */ ) { + print "
    • {$databaseSafe} is not a valid database name.
    "; + continue; + } elseif( $err != 1049 /* Database doesn't exist */ ) { + print "
    • Error selecting database {$databaseSafe}: {$err} "; + print htmlspecialchars( mysql_error() ) . "
    "; + continue; + } + print "
  • Attempting to create database...
  • "; + $res = $wgDatabase->query( "CREATE DATABASE `$wgDBname`" ); + if( !$res ) { + print "
  • Couldn't create database " . + htmlspecialchars( $wgDBname ) . + "; try with root access or check your username/pass.
  • \n"; + $errs["RootPW"] = "<- Enter"; + continue; + } + print "
  • Created database " . htmlspecialchars( $wgDBname ) . "
  • \n"; + } + $wgDatabase->selectDB( $wgDBname ); + } + else if ($conf->DBtype == 'postgres') { + if( version_compare( $myver, "8.0" ) < 0 ) { + dieout( "Postgres 8.0 or later is required. Aborting." ); + } + } + + if( $wgDatabase->tableExists( "cur" ) || $wgDatabase->tableExists( "revision" ) ) { + print "
  • There are already MediaWiki tables in this database. Checking if updates are needed...
  • \n"; + + if ( $conf->DBtype == 'mysql') { + # Determine existing default character set + if ( $wgDatabase->tableExists( "revision" ) ) { + $revision = $wgDatabase->escapeLike( $conf->DBprefix . 'revision' ); + $res = $wgDatabase->query( "SHOW TABLE STATUS LIKE '$revision'" ); + $row = $wgDatabase->fetchObject( $res ); + if ( !$row ) { + echo "
  • SHOW TABLE STATUS query failed!
  • \n"; + $existingSchema = false; + $existingEngine = false; + } else { + if ( preg_match( '/^latin1/', $row->Collation ) ) { + $existingSchema = 'mysql4'; + } elseif ( preg_match( '/^utf8/', $row->Collation ) ) { + $existingSchema = 'mysql5'; + } elseif ( preg_match( '/^binary/', $row->Collation ) ) { + $existingSchema = 'mysql5-binary'; + } else { + $existingSchema = false; + echo "
  • Warning: Unrecognised existing collation
  • \n"; + } + if ( isset( $row->Engine ) ) { + $existingEngine = $row->Engine; + } else { + $existingEngine = $row->Type; + } + } + if ( $existingSchema && $existingSchema != $conf->DBschema ) { + $encExisting = htmlspecialchars( $existingSchema ); + $encRequested = htmlspecialchars( $conf->DBschema ); + print "
  • Warning: you requested the $encRequested schema, " . + "but the existing database has the $encExisting schema. This upgrade script ". + "can't convert it, so it will remain $encExisting.
  • \n"; + $conf->setSchema( $existingSchema, $conf->DBengine ); + } + if ( $existingEngine && $existingEngine != $conf->DBengine ) { + $encExisting = htmlspecialchars( $existingEngine ); + $encRequested = htmlspecialchars( $conf->DBengine ); + print "
  • Warning: you requested the $encRequested storage " . + "engine, but the existing database uses the $encExisting engine. This upgrade " . + "script can't convert it, so it will remain $encExisting.
  • \n"; + $conf->setSchema( $conf->DBschema, $existingEngine ); + } + } + + # Create user if required + if ( $conf->Root ) { + $conn = $dbc->newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); + if ( $conn->isOpen() ) { + print "
  • DB user account ok
  • \n"; + $conn->close(); + } else { + print "
  • Granting user permissions..."; + if( $mysqlOldClient && $mysqlNewAuth ) { + print " If the next step fails, see http://dev.mysql.com/doc/mysql/en/old-client.html for help."; + } + print "
  • \n"; + $res = $wgDatabase->sourceFile( "../maintenance/users.sql" ); + if ($res !== true) dieout($res); + } + } + } + print "
\n";
+			chdir( ".." );
+			flush();
+			do_all_updates();
+			chdir( "config" );
+			print "
\n"; + print "
  • Finished update checks.
  • \n"; + // if tables don't yet exist + } else { + # Determine available storage engines if possible + if ( $conf->DBtype == 'mysql' && version_compare( $myver, "4.1.2", "ge" ) ) { + $res = $wgDatabase->query( 'SHOW ENGINES' ); + $found = false; + while ( $row = $wgDatabase->fetchObject( $res ) ) { + if ( $row->Engine == $conf->DBengine && ( $row->Support == 'YES' || $row->Support == 'DEFAULT' ) ) { + $found = true; + break; + } + } + $wgDatabase->freeResult( $res ); + if ( !$found && $conf->DBengine != 'MyISAM' ) { + echo "
  • Warning: " . htmlspecialchars( $conf->DBengine ) . + " storage engine not available, " . + "using MyISAM instead
  • \n"; + $conf->setSchema( $conf->DBschema, 'MyISAM' ); + } + } + + # FIXME: Check for errors + print "
  • Creating tables..."; + if ($conf->DBtype == 'mysql') { + $res = $wgDatabase->sourceFile( "../maintenance/tables.sql" ); + if ($res === true) { + print " done.
  • \n
  • Populating interwiki table... \n"; + $res = $wgDatabase->sourceFile( "../maintenance/interwiki.sql" ); + } + if ($res === true) { + print " done.
  • \n"; + } else { + print " FAILED\n"; + dieout( htmlspecialchars( $res ) ); + } + } elseif (is_callable(array($wgDatabase, 'setup_database'))) { + $wgDatabase->setup_database(); + } + else { + $errs["DBtype"] = "Do not know how to handle database type '$conf->DBtype'"; + continue; + } + + + if ( $conf->DBtype == 'ibm_db2' ) { + // Now that table creation is done, make sure everything is committed + // Do this before doing inserts through API + if ($wgDatabase->lastError()) { + print "
  • Errors encountered during table creation -- rolled back
  • \n"; + $wgDatabase->rollback(); + } + else { + print "
  • MediaWiki tables successfully created
  • \n"; + $wgDatabase->commit(); + } + } elseif ( $conf->DBtype == 'sqlite' ) { + // Ensure proper searchindex format. We have to do that separately because + // if SQLite is compiled without the FTS3 module, table creation syntax will be invalid. + sqlite_setup_searchindex(); + } + + print "
  • Initializing statistics...
  • \n"; + $wgDatabase->insert( 'site_stats', + array ( 'ss_row_id' => 1, + 'ss_total_views' => 0, + 'ss_total_edits' => 1, # Main page first edit + 'ss_good_articles' => 0, # Main page is not a good article - no internal link + 'ss_total_pages' => 1, # Main page + 'ss_users' => $conf->SysopName ? 1 : 0, # Sysop account, if created + 'ss_admins' => $conf->SysopName ? 1 : 0, # Sysop account, if created + 'ss_images' => 0 ) ); + + # Set up the "regular user" account *if we can, and if we need to* + if( $conf->Root and $conf->DBtype == 'mysql') { + # See if we need to + $wgDatabase2 = $dbc->newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); + if( $wgDatabase2->isOpen() ) { + # Nope, just close the test connection and continue + $wgDatabase2->close(); + echo( "
  • User " . htmlspecialchars( $wgDBuser ) . " exists. Skipping grants.
  • \n" ); + } else { + # Yes, so run the grants + echo( "
  • " . htmlspecialchars( "Granting user permissions to $wgDBuser on $wgDBname..." ) ); + $res = $wgDatabase->sourceFile( "../maintenance/users.sql" ); + if ( $res === true ) { + echo( " success.
  • \n" ); + } else { + echo( " FAILED.\n" ); + dieout( $res ); + } + } + } + + if( $conf->SysopName ) { + $u = User::newFromName( $conf->getSysopName() ); + if ( !$u ) { + print "
  • Warning: Skipped sysop account creation - invalid username!
  • \n"; + } + else if ( 0 == $u->idForName() ) { + $u->addToDatabase(); + $u->setPassword( $conf->getSysopPass() ); + $u->saveSettings(); + + $u->addGroup( "sysop" ); + $u->addGroup( "bureaucrat" ); + + print "
  • Created sysop account " . + htmlspecialchars( $conf->SysopName ) . ".
  • \n"; + } else { + print "
  • Could not create user - already exists!
  • \n"; + } + } else { + print "
  • Skipped sysop account creation, no name given.
  • \n"; + } + + $titleobj = Title::newFromText( wfMsgNoDB( "mainpage" ) ); + $article = new Article( $titleobj ); + $newid = $article->insertOn( $wgDatabase ); + $revision = new Revision( array( + 'page' => $newid, + 'text' => wfMsg( 'mainpagetext' ) . "\n\n" . wfMsgNoTrans( 'mainpagedocfooter' ), + 'comment' => '', + 'user' => 0, + 'user_text' => 'MediaWiki default', + ) ); + $revid = $revision->insertOn( $wgDatabase ); + $article->updateRevisionOn( $wgDatabase, $revision ); + } + + /* Write out the config file now that all is well */ + print "
  • \n"; + print "

    Creating LocalSettings.php...

    \n\n"; + $localSettings = "<" . "?php$endl$local"; + // Fix up a common line-ending problem (due to CVS on Windows) + $localSettings = str_replace( "\r\n", "\n", $localSettings ); + $f = fopen( "LocalSettings.php", 'xt' ); + + if( $f == false ) { + print( "
  • \n" ); + dieout( "

    Couldn't write out LocalSettings.php. Check that the directory permissions are correct and that there isn't already a file of that name here...

    \n" . + "

    Here's the file that would have been written, try to paste it into place manually:

    \n" . + "
    \n" . htmlspecialchars( $localSettings ) . "
    \n" ); + } + if(fwrite( $f, $localSettings ) ) { + fclose( $f ); + print "
    \n"; + writeSuccessMessage(); + print "\n"; + } else { + fclose( $f ); + dieout( "

    An error occured while writing the config/LocalSettings.php file. Check user rights and disk space then try again.

    \n" ); + } + + } while( false ); +} + +print "
\n"; +$mainListOpened = false; + +if( count( $errs ) ) { + /* Display options form */ + + if( $conf->posted ) { + echo "

Something's not quite right yet; make sure everything below is filled out correctly.

\n"; + } +?> + +
+ +

Site config

+ +
+
+ +
+

+ Preferably a short word without punctuation, i.e. "Wikipedia".
+ Will appear as the namespace name for "meta" pages, and throughout the interface. +

+
+

+ Displayed to users in some error messages, used as the return address for password reminders, and used as the default sender address of e-mail notifications. +

+ +
+ + +
+

+ Select the language for your wiki's interface. Some localizations aren't fully complete. Unicode (UTF-8) is used for all localizations. +

+ +
+ + +
    +
  • +
  • +
  • +
  • +
  • ScriptPath}/config/$script?License=cc&RightsUrl=[license_url]&RightsText=[license_name]&RightsCode=[license_code]&RightsIcon=[license_button]" ); + $icon = urlencode( "$wgServer$wgUploadPath/wiki.png" ); + $ccApp = htmlspecialchars( "http://creativecommons.org/license/?partner=$partner&exit_url=$exit&partner_icon_url=$icon" ); + print "choose"; + if( $conf->License == "cc" ) { ?> +
      +
    • RightsIcon ) . "\" alt='(Creative Commons icon)' />", "hidden" ); ?>
    • +
    • RightsText ), "hidden" ); ?>
    • +
    • RightsCode ), "hidden" ); ?>
    • +
    • RightsUrl ) . "\">" . htmlspecialchars( $conf->RightsUrl ) . "", "hidden" ); ?>
    • +
    + +
  • +
+
+

+ A notice, icon, and machine-readable copyright metadata will be displayed for the license you pick. +

+ + +
+ +
+
+ +
+
+ +
+

+ An admin can lock/delete pages, block users from editing, and do other maintenance tasks.
+ A new account will be added only when creating a new wiki database. +

+ The password cannot be the same as the username. +

+ +
+ + +
    +
  • + xcache ) { + echo "
  • "; + aField( $conf, 'Shm', 'XCache', 'radio', 'xcache' ); + echo "
  • \n"; + } + if ( $conf->apc ) { + echo "
  • "; + aField( $conf, "Shm", "APC", "radio", "apc" ); + echo "
  • \n"; + } + if ( $conf->eaccel ) { + echo "
  • "; + aField( $conf, "Shm", "eAccelerator", "radio", "eaccel" ); + echo "
  • \n"; + } + if ( $conf->dba ) { + echo "
  • "; + aField( $conf, "Shm", "DBA (not recommended)", "radio", "dba" ); + echo "
  • "; + } + ?> +
  • +
+
+
+

+ An object caching system such as memcached will provide a significant performance boost, + but needs to be installed. Provide the server addresses and ports in a comma-separated list. +

+ MediaWiki can also detect and support eAccelerator, APC, and XCache, but + these should not be used if the wiki will be running on multiple application servers. +

+ DBA (Berkeley-style DB) is generally slower than using no cache at all, and is only + recommended for testing. +

+
+ +

E-mail, e-mail notification and authentication setup

+ +
+
+ +
    +
  • +
  • +
+
+

+ Use this to disable all e-mail functions (password reminders, user-to-user e-mail, and e-mail notifications) + if sending mail doesn't work on your server. +

+ +
+ +
    +
  • +
  • +
+
+

+ The user-to-user e-mail feature (Special:Emailuser) lets the wiki act as a relay to allow users to exchange e-mail without publicly advertising their e-mail address. +

+
+ +
    +
  • +
  • +
  • +
+
+
+

+ For this feature to work, an e-mail address must be present for the user account, and the notification + options in the user's preferences must be enabled. Also note the + authentication option below. When testing the feature, keep in mind that your own changes will never trigger notifications to be sent to yourself.

+ +

There are additional options for fine tuning in /includes/DefaultSettings.php; copy these to your LocalSettings.php and edit them there to change them.

+
+ +
+ +
    +
  • +
  • +
+
+
+

If this option is enabled, users have to confirm their e-mail address using a magic link sent to them whenever they set or change it, and only authenticated e-mail addresses can receive mails from other users and/or + change notification mails. Setting this option is recommended for public wikis because of potential abuse of the e-mail features above.

+
+ +
+ +

Database config

+ +
+
+ +" . htmlspecialchars( $errs['DBpicktype'] ) . "\n"; + } +?> +
+
+ +
+
+ +
+

+ If your database server isn't on your web server, enter the name or IP address here. +

+
+ +
+
+
+
+
+

+ If you only have a single user account and database available, + enter those here. If you have database root access (see below) + you can specify new accounts/databases to be created. This account + will not be created if it pre-exists. If this is the case, ensure that it + has SELECT, INSERT, UPDATE, and DELETE permissions on the MediaWiki database. +

+ +
+ + checked="checked" /> +   +
+
+
+ +

+ If the database user specified above does not exist, or does not have access to create + the database (if needed) or tables within it, please check the box and provide details + of a superuser account, such as root, which does. +

+
+ + +
+
+

If you need to share one database between multiple wikis, or + between MediaWiki and another web application, you may choose to + add a prefix to all the table names to avoid conflicts.

+ +

Avoid exotic characters; something like mw_ is good.

+
+ +
+
Select one:
+
    +
  • +
  • +
+
+

+ InnoDB is best for public web installations, since it has good concurrency + support. MyISAM may be faster in single-user installations. MyISAM databases + tend to get corrupted more often than InnoDB databases. +

+
+
Select one:
+
    +
  • +
  • +
  • +
+
+

+ This option is ignored on upgrade, the same character set will be kept. +

+ WARNING: If you use backwards-compatible UTF-8 on MySQL 4.1+, and subsequently back up the database with mysqldump, it may destroy all non-ASCII characters, irreversibly corrupting your backups!. +

+ In binary mode, MediaWiki stores UTF-8 text to the database in binary fields. This is more efficient than MySQL's UTF-8 mode, and allows you to use the full range of Unicode characters. In UTF-8 mode, MySQL will know what character set your data is in, and can present and convert it appropriately, but it won't let you store characters above the Basic Multilingual Plane. +

+ + + +
+
+
+
+

The username specified above (at "DB username") will have its search path set to the above schemas, + so it is recommended that you create a new user. The above schemas are generally correct: + only change them if you are sure you need to.

+
+ + + +
+
+

SQLite stores table data into files in the + filesystem.

+ +

This directory must exist and be writable by the web server.

+
+ + + +
+
+

If you need to share one database between multiple wikis, or + between MediaWiki and another web application, you may choose to + add a prefix to all the table names to avoid conflicts.

+ +

Avoid exotic characters; something like mw_ is good.

+
+ + + +
+
+
Select one:
+
    +
  • +
  • +
+
+

If you need to share one database between multiple wikis, or + between MediaWiki and another web application, you may specify + a different schema to avoid conflicts.

+
+ + + +
+
+

If you need to share one database between multiple wikis, or + between MediaWiki and another web application, you may choose to + add a prefix to all the table names to avoid conflicts.

+ +

Avoid exotic characters; something like mw_ is good.

+
+
+
+ + +
+ + +
+
+
+ + +

Installation successful!

+

To complete the installation, please do the following: +

    +
  1. Download config/LocalSettings.php with your FTP client or file manager
  2. +
  3. Upload it to the parent directory
  4. +
  5. Delete config/LocalSettings.php
  6. +
  7. Start using your wiki! +
+

If you are in a shared hosting environment, do not just move LocalSettings.php +remotely. LocalSettings.php is currently owned by the user your webserver is running under, +which means that anyone on the same server can read your database password! Downloading +it and uploading it again will hopefully change the ownership to a user ID specific to you.

+
+HTML; + } else { + echo << +

+Installation successful! +Move the config/LocalSettings.php file to the parent directory, then follow + this link to your wiki.

+

You should change file permissions for LocalSettings.php as required to +prevent other users on the server reading passwords and altering configuration data.

+
+HTML; + } +} + + +function escapePhpString( $string ) { + if ( is_array( $string ) || is_object( $string ) ) { + return false; + } + return strtr( $string, + array( + "\n" => "\\n", + "\r" => "\\r", + "\t" => "\\t", + "\\" => "\\\\", + "\$" => "\\\$", + "\"" => "\\\"" + )); +} + +function writeLocalSettings( $conf ) { + $conf->PasswordSender = $conf->EmergencyContact; + $magic = ($conf->ImageMagick ? "" : "# "); + $convert = ($conf->ImageMagick ? $conf->ImageMagick : "/usr/bin/convert" ); + $rights = ($conf->RightsUrl) ? "" : "# "; + $hashedUploads = $conf->safeMode ? '' : '# '; + $dir = realpath( $conf->SQLiteDataDir ); + if ( !$dir ) { + $dir = $conf->SQLiteDataDir; // dumb realpath sometimes fails + } + $sqliteDataDir = escapePhpString( $dir ); + + if ( $conf->ShellLocale ) { + $locale = ''; + } else { + $locale = '# '; + $conf->ShellLocale = 'en_US.UTF-8'; + } + + switch ( $conf->Shm ) { + case 'memcached': + $cacheType = 'CACHE_MEMCACHED'; + $mcservers = var_export( $conf->MCServerArray, true ); + break; + case 'xcache': + case 'apc': + case 'eaccel': + $cacheType = 'CACHE_ACCEL'; + $mcservers = 'array()'; + break; + case 'dba': + $cacheType = 'CACHE_DBA'; + $mcservers = 'array()'; + break; + default: + $cacheType = 'CACHE_NONE'; + $mcservers = 'array()'; + } + + if ( $conf->Email == 'email_enabled' ) { + $enableemail = 'true'; + $enableuseremail = ( $conf->Emailuser == 'emailuser_enabled' ) ? 'true' : 'false' ; + $eauthent = ( $conf->Eauthent == 'eauthent_enabled' ) ? 'true' : 'false' ; + switch ( $conf->Enotif ) { + case 'enotif_usertalk': + $enotifusertalk = 'true'; + $enotifwatchlist = 'false'; + break; + case 'enotif_allpages': + $enotifusertalk = 'true'; + $enotifwatchlist = 'true'; + break; + default: + $enotifusertalk = 'false'; + $enotifwatchlist = 'false'; + } + } else { + $enableuseremail = 'false'; + $enableemail = 'false'; + $eauthent = 'false'; + $enotifusertalk = 'false'; + $enotifwatchlist = 'false'; + } + + $file = @fopen( "/dev/urandom", "r" ); + if ( $file ) { + $secretKey = bin2hex( fread( $file, 32 ) ); + fclose( $file ); + } else { + $secretKey = ""; + for ( $i=0; $i<8; $i++ ) { + $secretKey .= dechex(mt_rand(0, 0x7fffffff)); + } + print "
  • Warning: \$wgSecretKey key is insecure, generated with mt_rand(). Consider changing it manually.
  • \n"; + } + + # Add slashes to strings for double quoting + $slconf = wfArrayMap( "escapePhpString", get_object_vars( $conf ) ); + if( $conf->License == 'gfdl1_2' || $conf->License == 'pd' || $conf->License == 'gfdl1_3' ) { + # Needs literal string interpolation for the current style path + $slconf['RightsIcon'] = $conf->RightsIcon; + } + + if( $conf->DBtype == 'mysql' ) { + $dbsettings = +"# MySQL specific settings +\$wgDBprefix = \"{$slconf['DBprefix']}\"; + +# MySQL table options to use during installation or update +\$wgDBTableOptions = \"{$slconf['DBTableOptions']}\"; + +# Experimental charset support for MySQL 4.1/5.0. +\$wgDBmysql5 = {$conf->DBmysql5};"; + } elseif( $conf->DBtype == 'postgres' ) { + $dbsettings = +"# Postgres specific settings +\$wgDBport = \"{$slconf['DBport']}\"; +\$wgDBmwschema = \"{$slconf['DBpgschema']}\"; +\$wgDBts2schema = \"{$slconf['DBts2schema']}\";"; + } elseif( $conf->DBtype == 'sqlite' ) { + $dbsettings = +"# SQLite-specific settings +\$wgSQLiteDataDir = \"{$sqliteDataDir}\";"; + } elseif( $conf->DBtype == 'mssql' ) { + $dbsettings = +"# MSSQL specific settings +\$wgDBprefix = \"{$slconf['DBprefix2']}\";"; + } elseif( $conf->DBtype == 'ibm_db2' ) { + $dbsettings = +"# DB2 specific settings +\$wgDBport_db2 = \"{$slconf['DBport_db2']}\"; +\$wgDBmwschema = \"{$slconf['DBdb2schema']}\"; +\$wgDBcataloged = \"{$slconf['DBcataloged']}\";"; + } elseif( $conf->DBtype == 'oracle' ) { + $dbsettings = +"# Oracle specific settings +\$wgDBprefix = \"{$slconf['DBprefix_ora']}\";"; + } else { + // ummm... :D + $dbsettings = ''; + } + + + $localsettings = " +# This file was automatically generated by the MediaWiki installer. +# If you make manual changes, please keep track in case you need to +# recreate them later. +# +# See includes/DefaultSettings.php for all configurable settings +# and their default values, but don't forget to make changes in _this_ +# file, not there. +# +# Further documentation for configuration settings may be found at: +# http://www.mediawiki.org/wiki/Manual:Configuration_settings + +# If you customize your file layout, set \$IP to the directory that contains +# the other MediaWiki files. It will be used as a base to locate files. +if( defined( 'MW_INSTALL_PATH' ) ) { + \$IP = MW_INSTALL_PATH; +} else { + \$IP = dirname( __FILE__ ); +} + +\$path = array( \$IP, \"\$IP/includes\", \"\$IP/languages\" ); +set_include_path( implode( PATH_SEPARATOR, \$path ) . PATH_SEPARATOR . get_include_path() ); + +require_once( \"\$IP/includes/DefaultSettings.php\" ); + +if ( \$wgCommandLineMode ) { + if ( isset( \$_SERVER ) && array_key_exists( 'REQUEST_METHOD', \$_SERVER ) ) { + die( \"This script must be run from the command line\\n\" ); + } +} +## Uncomment this to disable output compression +# \$wgDisableOutputCompression = true; + +\$wgSitename = \"{$slconf['Sitename']}\"; + +## The URL base path to the directory containing the wiki; +## defaults for all runtime URL paths are based off of this. +## For more information on customizing the URLs please see: +## http://www.mediawiki.org/wiki/Manual:Short_URL +\$wgScriptPath = \"{$slconf['ScriptPath']}\"; +\$wgScriptExtension = \"{$slconf['ScriptExtension']}\"; + +## The relative URL path to the skins directory +\$wgStylePath = \"\$wgScriptPath/skins\"; + +## The relative URL path to the logo. Make sure you change this from the default, +## or else you'll overwrite your logo when you upgrade! +\$wgLogo = \"\$wgStylePath/common/images/wiki.png\"; + +## UPO means: this is also a user preference option + +\$wgEnableEmail = $enableemail; +\$wgEnableUserEmail = $enableuseremail; # UPO + +\$wgEmergencyContact = \"{$slconf['EmergencyContact']}\"; +\$wgPasswordSender = \"{$slconf['PasswordSender']}\"; + +\$wgEnotifUserTalk = $enotifusertalk; # UPO +\$wgEnotifWatchlist = $enotifwatchlist; # UPO +\$wgEmailAuthentication = $eauthent; + +## Database settings +\$wgDBtype = \"{$slconf['DBtype']}\"; +\$wgDBserver = \"{$slconf['DBserver']}\"; +\$wgDBname = \"{$slconf['DBname']}\"; +\$wgDBuser = \"{$slconf['DBuser']}\"; +\$wgDBpassword = \"{$slconf['DBpassword']}\"; + +{$dbsettings} + +## Shared memory settings +\$wgMainCacheType = $cacheType; +\$wgMemCachedServers = $mcservers; + +## To enable image uploads, make sure the 'images' directory +## is writable, then set this to true: +\$wgEnableUploads = false; +{$magic}\$wgUseImageMagick = true; +{$magic}\$wgImageMagickConvertCommand = \"{$convert}\"; + +## If you use ImageMagick (or any other shell command) on a +## Linux server, this will need to be set to the name of an +## available UTF-8 locale +{$locale}\$wgShellLocale = \"{$slconf['ShellLocale']}\"; + +## If you want to use image uploads under safe mode, +## create the directories images/archive, images/thumb and +## images/temp, and make them all writable. Then uncomment +## this, if it's not already uncommented: +{$hashedUploads}\$wgHashedUploadDirectory = false; + +## If you have the appropriate support software installed +## you can enable inline LaTeX equations: +\$wgUseTeX = false; + +## Set \$wgCacheDirectory to a writable directory on the web server +## to make your wiki go slightly faster. The directory should not +## be publically accessible from the web. +#\$wgCacheDirectory = \"\$IP/cache\"; + +\$wgLocalInterwiki = strtolower( \$wgSitename ); + +\$wgLanguageCode = \"{$slconf['LanguageCode']}\"; + +\$wgSecretKey = \"$secretKey\"; + +## Default skin: you can change the default skin. Use the internal symbolic +## names, ie 'vector', 'monobook': +\$wgDefaultSkin = 'monobook'; + +## For attaching licensing metadata to pages, and displaying an +## appropriate copyright notice / icon. GNU Free Documentation +## License and Creative Commons licenses are supported so far. +{$rights}\$wgEnableCreativeCommonsRdf = true; +\$wgRightsPage = \"\"; # Set to the title of a wiki page that describes your license/copyright +\$wgRightsUrl = \"{$slconf['RightsUrl']}\"; +\$wgRightsText = \"{$slconf['RightsText']}\"; +\$wgRightsIcon = \"{$slconf['RightsIcon']}\"; +# \$wgRightsCode = \"{$slconf['RightsCode']}\"; # Not yet used + +\$wgDiff3 = \"{$slconf['diff3']}\"; + +# When you make changes to this configuration file, this will make +# sure that cached pages are cleared. +\$wgCacheEpoch = max( \$wgCacheEpoch, gmdate( 'YmdHis', @filemtime( __FILE__ ) ) ); +"; ## End of setting the $localsettings string + + // Keep things in Unix line endings internally; + // the system will write out as local text type. + return str_replace( "\r\n", "\n", $localsettings ); +} + +function dieout( $text ) { + global $mainListOpened; + if( $mainListOpened ) echo( "" ); + if( $text != '' && substr( $text, 0, 2 ) != '$text

    \n"; + } else { + echo $text; + } + die( "\n\n
    \n
    \n\n\n\n" ); +} + +function importVar( &$var, $name, $default = "" ) { + if( isset( $var[$name] ) ) { + $retval = $var[$name]; + if ( get_magic_quotes_gpc() ) { + $retval = stripslashes( $retval ); + } + } else { + $retval = $default; + } + taint( $retval ); + return $retval; +} + +function importPost( $name, $default = "" ) { + return importVar( $_POST, $name, $default ); +} + +function importCheck( $name ) { + return isset( $_POST[$name] ); +} + +function importRequest( $name, $default = "" ) { + return importVar( $_REQUEST, $name, $default ); +} + +function aField( &$conf, $field, $text, $type = "text", $value = "", $onclick = '' ) { + static $radioCount = 0; + if( $type != "" ) { + $xtype = "type=\"$type\""; + } else { + $xtype = ""; + } + + $id = $field; + $nolabel = ($type == "radio") || ($type == "hidden"); + + if ($type == 'radio') + $id .= $radioCount++; + + if( !$nolabel ) { + echo ""; + } + + if( $type == "radio" && $value == $conf->$field ) { + $checked = "checked='checked'"; + } else { + $checked = ""; + } + echo "$field ); + } + + + echo "\" />"; + if( $nolabel ) { + echo ""; + } + + global $errs; + if(isset($errs[$field])) { + echo "" . htmlspecialchars( $errs[$field] ) . "\n"; + } +} + +function getLanguageList() { + global $wgDummyLanguageCodes; + + $codes = array(); + foreach ( Language::getLanguageNames() as $code => $name ) { + if( in_array( $code, $wgDummyLanguageCodes ) ) continue; + $codes[$code] = $code . ' - ' . $name; + } + ksort( $codes ); + return $codes; +} + +#Check for location of an executable +# @param string $loc single location to check +# @param array $names filenames to check for. +# @param mixed $versioninfo array of details to use when checking version, use false for no version checking +function locate_executable($loc, $names, $versioninfo = false) { + if (!is_array($names)) + $names = array($names); + + foreach ($names as $name) { + $command = "$loc".DIRECTORY_SEPARATOR."$name"; + if (@file_exists($command)) { + if (!$versioninfo) + return $command; + + $file = str_replace('$1', $command, $versioninfo[0]); + if (strstr(`$file`, $versioninfo[1]) !== false) + return $command; + } + } + return false; +} + +# Test a memcached server +function testMemcachedServer( $server ) { + $hostport = explode(":", $server); + $errstr = false; + $fp = false; + if ( !function_exists( 'fsockopen' ) ) { + $errstr = "Can't connect to memcached, fsockopen() not present"; + } + if ( !$errstr && count( $hostport ) != 2 ) { + $errstr = 'Please specify host and port'; + } + if ( !$errstr ) { + list( $host, $port ) = $hostport; + $errno = 0; + $fsockerr = ''; + + $fp = @fsockopen( $host, $port, $errno, $fsockerr, 1.0 ); + if ( $fp === false ) { + $errstr = "Cannot connect to memcached on $host:$port : $fsockerr"; + } + } + if ( !$errstr ) { + $command = "version\r\n"; + $bytes = fwrite( $fp, $command ); + if ( $bytes != strlen( $command ) ) { + $errstr = "Cannot write to memcached socket on $host:$port"; + } + } + if ( !$errstr ) { + $expected = "VERSION "; + $response = fread( $fp, strlen( $expected ) ); + if ( $response != $expected ) { + $errstr = "Didn't get correct memcached response from $host:$port"; + } + } + if ( $fp ) { + fclose( $fp ); + } + if ( !$errstr ) { + echo "
  • Connected to memcached on " . htmlspecialchars( "$host:$port" ) ." successfully
  • "; + } + return $errstr; +} + +function database_picker($conf) { + global $ourdb; + print "\n"; + foreach(array_keys($ourdb) as $db) { + if ($ourdb[$db]['havedriver']) { + print "\t
  • "; + aField( $conf, "DBtype", $ourdb[$db]['fullname'], 'radio', $db, 'onclick'); + print "
  • \n"; + } + } + print "\n\t"; +} + +function database_switcher($db) { + global $ourdb; + $color = $ourdb[$db]['bgcolor']; + $full = $ourdb[$db]['fullname']; + print "
    $full-specific options\n"; +} + +function printListItem( $item ) { + print "
  • $item
  • "; +} + +# Determine a suitable value for $wgShellLocale +function getShellLocale( $wikiLang ) { + # Give up now if we're in safe mode or open_basedir + # It's theoretically possible but tricky to work with + if ( wfIniGetBool( "safe_mode" ) || ini_get( 'open_basedir' ) || !function_exists('exec') ) { + return false; + } + + $os = php_uname( 's' ); + $supported = array( 'Linux', 'SunOS', 'HP-UX' ); # Tested these + if ( !in_array( $os, $supported ) ) { + return false; + } + + # Get a list of available locales + $lines = $ret = false; + exec( '/usr/bin/locale -a', $lines, $ret ); + if ( $ret ) { + return false; + } + + $lines = wfArrayMap( 'trim', $lines ); + $candidatesByLocale = array(); + $candidatesByLang = array(); + foreach ( $lines as $line ) { + if ( $line === '' ) { + continue; + } + if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) { + continue; + } + list( $all, $lang, $territory, $charset, $modifier ) = $m; + $candidatesByLocale[$m[0]] = $m; + $candidatesByLang[$lang][] = $m; + } + + # Try the current value of LANG + if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) { + return getenv( 'LANG' ); + } + + # Try the most common ones + $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ); + foreach ( $commonLocales as $commonLocale ) { + if ( isset( $candidatesByLocale[$commonLocale] ) ) { + return $commonLocale; + } + } + + # Is there an available locale in the Wiki's language? + if ( isset( $candidatesByLang[$wikiLang] ) ) { + $m = reset( $candidatesByLang[$wikiLang] ); + return $m[0]; + } + + # Are there any at all? + if ( count( $candidatesByLocale ) ) { + $m = reset( $candidatesByLocale ); + return $m[0]; + } + + # Give up + return false; +} + +function wfArrayMap( $function, $input ) { + $ret = array_map( $function, $input ); + foreach ( $ret as $key => $value ) { + $taint = istainted( $input[$key] ); + if ( $taint ) { + taint( $ret[$key], $taint ); + } + } + return $ret; +} + +?> + +
    +
    +

    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. + or read it online

    +
    + + + + +
    + + +
    + +

    MediaWiki is Copyright © 2001-2009 by 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.

    +
    +
    + + + + + diff --git a/config/index.php b/config/index.php index 85fdb86f..d913bbb1 100644 --- a/config/index.php +++ b/config/index.php @@ -19,13 +19,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html -error_reporting( E_ALL ); -header( "Content-type: text/html; charset=utf-8" ); -@ini_set( "display_errors", true ); - -# In case of errors, let output be clean. -$wgRequestTime = microtime( true ); - # Attempt to set up the include path, to fix problems with relative includes $IP = dirname( dirname( __FILE__ ) ); define( 'MW_INSTALL_PATH', $IP ); @@ -34,2177 +27,17 @@ define( 'MW_INSTALL_PATH', $IP ); define( "MEDIAWIKI", true ); define( "MEDIAWIKI_INSTALL", true ); -// Run version checks before including other files -// so people don't see a scary parse error. -require_once( "$IP/install-utils.inc" ); -install_version_checks(); - -require_once( "$IP/includes/Defines.php" ); -require_once( "$IP/includes/DefaultSettings.php" ); -require_once( "$IP/includes/AutoLoader.php" ); -require_once( "$IP/includes/MagicWord.php" ); -require_once( "$IP/includes/Namespace.php" ); -require_once( "$IP/includes/ProfilerStub.php" ); -require_once( "$IP/includes/GlobalFunctions.php" ); -require_once( "$IP/includes/Hooks.php" ); -require_once( "$IP/includes/Exception.php" ); - -# If we get an exception, the user needs to know -# all the details -$wgShowExceptionDetails = true; -$wgShowSQLErrors = true; -wfInstallExceptionHandler(); -## Databases we support: - -$ourdb = array(); -$ourdb['mysql']['fullname'] = 'MySQL'; -$ourdb['mysql']['havedriver'] = 0; -$ourdb['mysql']['compile'] = 'mysql'; -$ourdb['mysql']['bgcolor'] = '#ffe5a7'; -$ourdb['mysql']['rootuser'] = 'root'; - -$ourdb['postgres']['fullname'] = 'PostgreSQL'; -$ourdb['postgres']['havedriver'] = 0; -$ourdb['postgres']['compile'] = 'pgsql'; -$ourdb['postgres']['bgcolor'] = '#aaccff'; -$ourdb['postgres']['rootuser'] = 'postgres'; - -$ourdb['sqlite']['fullname'] = 'SQLite'; -$ourdb['sqlite']['havedriver'] = 0; -$ourdb['sqlite']['compile'] = 'pdo_sqlite'; -$ourdb['sqlite']['bgcolor'] = '#b1ebb1'; -$ourdb['sqlite']['rootuser'] = ''; - -$ourdb['mssql']['fullname'] = 'MSSQL'; -$ourdb['mssql']['havedriver'] = 0; -$ourdb['mssql']['compile'] = 'mssql not ready'; # Change to 'mssql' after includes/DatabaseMssql.php added; -$ourdb['mssql']['bgcolor'] = '#ffc0cb'; -$ourdb['mssql']['rootuser'] = 'administrator'; - -$ourdb['ibm_db2']['fullname'] = 'DB2'; -$ourdb['ibm_db2']['havedriver'] = 0; -$ourdb['ibm_db2']['compile'] = 'ibm_db2'; -$ourdb['ibm_db2']['bgcolor'] = '#ffeba1'; -$ourdb['ibm_db2']['rootuser'] = 'db2admin'; - -?> - - - - - - MediaWiki <?php echo htmlspecialchars( $wgVersion ); ?> Installation - - - - - -
    -
    -
    -
    - -

    MediaWiki Installation

    - -Setup has completed, your wiki is configured.

    -

    Please delete the /config directory for extra security.

    " ); -} - -if( file_exists( "./LocalSettings.php" ) ) { - writeSuccessMessage(); - dieout( '' ); -} - -if( !is_writable( "." ) ) { - dieout( "

    Can't write config file, aborting

    - -

    In order to configure the wiki you have to make the config subdirectory - writable by the web server. Once configuration is done you'll move the created - LocalSettings.php to the parent directory, and for added safety you can - then remove the config subdirectory entirely.

    - -

    To make the directory writable on a Unix/Linux system:

    - -
    -	cd /path/to/wiki
    -	chmod a+w config
    -	
    - -

    Afterwards retry to start the setup.

    " ); -} - - -require_once( "$IP/install-utils.inc" ); -require_once( "$IP/maintenance/updaters.inc" ); - -class ConfigData { - function getEncoded( $data ) { - # removing latin1 support, no need... - return $data; - } - function getSitename() { return $this->getEncoded( $this->Sitename ); } - function getSysopName() { return $this->getEncoded( $this->SysopName ); } - function getSysopPass() { return $this->getEncoded( $this->SysopPass ); } - - function setSchema( $schema, $engine ) { - $this->DBschema = $schema; - if ( !preg_match( '/^\w*$/', $engine ) ){ - $engine = 'InnoDB'; - } - switch ( $this->DBschema ) { - case 'mysql5': - $this->DBTableOptions = "ENGINE=$engine, DEFAULT CHARSET=utf8"; - $this->DBmysql5 = 'true'; - break; - case 'mysql5-binary': - $this->DBTableOptions = "ENGINE=$engine, DEFAULT CHARSET=binary"; - $this->DBmysql5 = 'true'; - break; - default: - $this->DBTableOptions = "TYPE=$engine"; - $this->DBmysql5 = 'false'; - } - $this->DBengine = $engine; - - # Set the global for use during install - global $wgDBTableOptions; - $wgDBTableOptions = $this->DBTableOptions; - } -} - -?> - - - - -

    Checking environment...

    -

    Please include all of the lines below when reporting installation problems.

    -
      -PHP " . htmlspecialchars( phpversion() ) . " installed\n"; - -error_reporting( 0 ); -$phpdatabases = array(); -foreach (array_keys($ourdb) as $db) { - $compname = $ourdb[$db]['compile']; - if( extension_loaded( $compname ) || ( mw_have_dl() && dl( "{$compname}." . PHP_SHLIB_SUFFIX ) ) ) { - array_push($phpdatabases, $db); - $ourdb[$db]['havedriver'] = 1; - } -} -error_reporting( E_ALL ); - -if (!$phpdatabases) { - print "Could not find a suitable database driver!
        "; - foreach (array_keys($ourdb) AS $db) { - $comp = $ourdb[$db]['compile']; - $full = $ourdb[$db]['fullname']; - print "
      • For $full, compile PHP using --with-$comp, " - ."or install the $comp.so module
      • \n"; - } - echo '
      '; - dieout( '' ); -} - -print "
    • Found database drivers for:"; -$DefaultDBtype = ''; -foreach (array_keys($ourdb) AS $db) { - if ($ourdb[$db]['havedriver']) { - if ( $DefaultDBtype == '' ) { - $DefaultDBtype = $db; - } - print " ".$ourdb[$db]['fullname']; - } -} -print "
    • \n"; - -if( wfIniGetBool( "register_globals" ) ) { - ?> -
    • -
      - Warning: - PHP's register_globals option is enabled. Disable it if you can. -
      - MediaWiki will work, but your server is more exposed to PHP-based security vulnerabilities. -
    • -
    • Fatal: magic_quotes_runtime is active! - This option corrupts data input unpredictably; you cannot install or use - MediaWiki unless this option is disabled.
    • -
    • Fatal: magic_quotes_sybase is active! - This option corrupts data input unpredictably; you cannot install or use - MediaWiki unless this option is disabled.
    • -
    • Fatal: mbstring.func_overload is active! - This option causes errors and may corrupt data unpredictably; - you cannot install or use MediaWiki unless this option is disabled.
    • -
    • Fatal: zend.ze1_compatibility_mode is active! - This option causes horrible bugs with MediaWiki; you cannot install or use - MediaWiki unless this option is disabled.
    • - safeMode = true; - ?> -
    • Warning: PHP's - safe mode is active. - You may have problems caused by this, particularly if using image uploads. -
    • - safeMode = false; -} - -$sapi = htmlspecialchars( php_sapi_name() ); -print "
    • PHP server API is $sapi; "; -$script = defined('MW_INSTALL_PHP5_EXT') ? 'index.php5' : 'index.php'; -if( $wgUsePathInfo ) { - print "ok, using pretty URLs ($script/Page_Title)"; -} else { - print "using ugly URLs ($script?title=Page_Title)"; -} -print "
    • \n"; - -$conf->xml = function_exists( "utf8_encode" ); -if( $conf->xml ) { - print "
    • Have XML / Latin1-UTF-8 conversion support.
    • \n"; -} else { - dieout( "PHP's XML module is missing; the wiki requires functions in - this module and won't work in this configuration. - If you're running Mandrake, install the php-xml package." ); +# Check for PHP 5 +if ( !function_exists( 'version_compare' ) + || version_compare( phpversion(), '5.0.0' ) < 0 +) { + define( 'MW_PHP4', '1' ); + require( "$IP/includes/DefaultSettings.php" ); + require( "$IP/includes/templates/PHP4.php" ); + exit; } -# Check for session support -if( !function_exists( 'session_name' ) ) - dieout( "PHP's session module is missing. MediaWiki requires session support in order to function." ); - -# session.save_path doesn't *have* to be set, but if it is, and it's -# not valid/writable/etc. then it can cause problems -$sessionSavePath = mw_get_session_save_path(); -$ssp = htmlspecialchars( $sessionSavePath ); -# Warn the user if it's not set, but let them proceed -if( !$sessionSavePath ) { - print "
    • Warning: A value for session.save_path - has not been set in PHP.ini. If the default value causes problems with - saving session data, set it to a valid path which is read/write/execute - for the user your web server is running under.
    • "; -} elseif ( is_dir( $sessionSavePath ) && is_writable( $sessionSavePath ) ) { - # All good? Let the user know - print "
    • Session save path ({$ssp}) appears to be valid.
    • "; -} else { - # Something not right? Warn the user, but let them proceed - print "
    • Warning: Your session.save_path value ({$ssp}) - appears to be invalid or is not writable. PHP needs to be able to save data to - this location for correct session operation.
    • "; -} - -# Check for PCRE support -if( !function_exists( 'preg_match' ) ) - dieout( "The PCRE support module appears to be missing. MediaWiki requires the - Perl-compatible regular expression functions." ); - -$memlimit = ini_get( "memory_limit" ); -$conf->raiseMemory = false; -if( empty( $memlimit ) || $memlimit == -1 ) { - print "
    • PHP is configured with no memory_limit.
    • \n"; -} else { - print "
    • PHP's memory_limit is " . htmlspecialchars( $memlimit ) . ". "; - $n = intval( $memlimit ); - if( preg_match( '/^([0-9]+)[Mm]$/', trim( $memlimit ), $m ) ) { - $n = intval( $m[1] * (1024*1024) ); - } - if( $n < 20*1024*1024 ) { - print "Attempting to raise limit to 20M... "; - if( false === ini_set( "memory_limit", "20M" ) ) { - print "failed.
      " . htmlspecialchars( $memlimit ) . " seems too low, installation may fail!"; - } else { - $conf->raiseMemory = true; - print "ok."; - } - } - print "
    • \n"; -} - -$conf->turck = function_exists( 'mmcache_get' ); -if ( $conf->turck ) { - print "
    • Turck MMCache installed
    • \n"; -} - -$conf->xcache = function_exists( 'xcache_get' ); -if( $conf->xcache ) - print "
    • XCache installed
    • \n"; - -$conf->apc = function_exists('apc_fetch'); -if ($conf->apc ) { - print "
    • APC installed
    • \n"; -} - -$conf->eaccel = function_exists( 'eaccelerator_get' ); -if ( $conf->eaccel ) { - $conf->turck = 'eaccelerator'; - print "
    • eAccelerator installed
    • \n"; -} - -$conf->dba = function_exists( 'dba_open' ); - -if( !( $conf->turck || $conf->eaccel || $conf->apc || $conf->xcache ) ) { - echo( '
    • Couldn\'t find Turck MMCache, - eAccelerator, - APC or XCache; - cannot use these for object caching.
    • ' ); -} - -$conf->diff3 = false; -$diff3locations = array_merge( - array( - "/usr/bin", - "/usr/local/bin", - "/opt/csw/bin", - "/usr/gnu/bin", - "/usr/sfw/bin" ), - explode( PATH_SEPARATOR, getenv( "PATH" ) ) ); -$diff3names = array( "gdiff3", "diff3", "diff3.exe" ); - -$diff3versioninfo = array( '$1 --version 2>&1', 'diff3 (GNU diffutils)' ); -foreach ($diff3locations as $loc) { - $exe = locate_executable($loc, $diff3names, $diff3versioninfo); - if ($exe !== false) { - $conf->diff3 = $exe; - break; - } -} - -if ($conf->diff3) - print "
    • Found GNU diff3: $conf->diff3.
    • "; -else - print "
    • GNU diff3 not found.
    • "; - -$conf->ImageMagick = false; -$imcheck = array( "/usr/bin", "/opt/csw/bin", "/usr/local/bin", "/sw/bin", "/opt/local/bin" ); -foreach( $imcheck as $dir ) { - $im = "$dir/convert"; - if( @file_exists( $im ) ) { - print "
    • Found ImageMagick: $im; image thumbnailing will be enabled if you enable uploads.
    • \n"; - $conf->ImageMagick = $im; - break; - } -} - -$conf->HaveGD = function_exists( "imagejpeg" ); -if( $conf->HaveGD ) { - print "
    • Found GD graphics library built-in"; - if( !$conf->ImageMagick ) { - print ", image thumbnailing will be enabled if you enable uploads"; - } - print ".
    • \n"; -} else { - if( !$conf->ImageMagick ) { - print "
    • Couldn't find GD library or ImageMagick; image thumbnailing disabled.
    • \n"; - } -} - -$conf->IP = dirname( dirname( __FILE__ ) ); -print "
    • Installation directory: " . htmlspecialchars( $conf->IP ) . "
    • \n"; - - -// PHP_SELF isn't available sometimes, such as when PHP is CGI but -// cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME -// to get the path to the current script... hopefully it's reliable. SIGH -$path = ($_SERVER["PHP_SELF"] === '') - ? $_SERVER["SCRIPT_NAME"] - : $_SERVER["PHP_SELF"]; - -$conf->ScriptPath = preg_replace( '{^(.*)/config.*$}', '$1', $path ); -print "
    • Script URI path: " . htmlspecialchars( $conf->ScriptPath ) . "
    • \n"; - - - -// We may be installing from *.php5 extension file, if so, print message -$conf->ScriptExtension = '.php'; -if (defined('MW_INSTALL_PHP5_EXT')) { - $conf->ScriptExtension = '.php5'; - print "
    • Installing MediaWiki with php5 file extensions
    • \n"; -} else { - print "
    • Installing MediaWiki with php file extensions
    • \n"; -} - - -print "
    • Environment checked. You can install MediaWiki.
    • \n"; - $conf->posted = ($_SERVER["REQUEST_METHOD"] == "POST"); - - $conf->Sitename = ucfirst( importPost( "Sitename", "" ) ); - $defaultEmail = empty( $_SERVER["SERVER_ADMIN"] ) - ? 'root@localhost' - : $_SERVER["SERVER_ADMIN"]; - $conf->EmergencyContact = importPost( "EmergencyContact", $defaultEmail ); - $conf->DBtype = importPost( "DBtype", $DefaultDBtype ); - if ( !isset( $ourdb[$conf->DBtype] ) ) { - $conf->DBtype = $DefaultDBtype; - } - - $conf->DBserver = importPost( "DBserver", "localhost" ); - $conf->DBname = importPost( "DBname", "wikidb" ); - $conf->DBuser = importPost( "DBuser", "wikiuser" ); - $conf->DBpassword = importPost( "DBpassword" ); - $conf->DBpassword2 = importPost( "DBpassword2" ); - $conf->SysopName = importPost( "SysopName", "WikiSysop" ); - $conf->SysopPass = importPost( "SysopPass" ); - $conf->SysopPass2 = importPost( "SysopPass2" ); - $conf->RootUser = importPost( "RootUser", "root" ); - $conf->RootPW = importPost( "RootPW", "" ); - $useRoot = importCheck( 'useroot', false ); - $conf->LanguageCode = importPost( "LanguageCode", "en" ); - ## MySQL specific: - $conf->DBprefix = importPost( "DBprefix" ); - $conf->setSchema( - importPost( "DBschema", "mysql5-binary" ), - importPost( "DBengine", "InnoDB" ) ); - - ## Postgres specific: - $conf->DBport = importPost( "DBport", "5432" ); - $conf->DBmwschema = importPost( "DBmwschema", "mediawiki" ); - $conf->DBts2schema = importPost( "DBts2schema", "public" ); - - ## SQLite specific - $conf->SQLiteDataDir = importPost( "SQLiteDataDir", "" ); - - ## MSSQL specific - // We need a second field so it doesn't overwrite the MySQL one - $conf->DBprefix2 = importPost( "DBprefix2" ); - - ## DB2 specific: - // New variable in order to have a different default port number - $conf->DBport_db2 = importPost( "DBport_db2", "50000" ); - $conf->DBmwschema = importPost( "DBmwschema", "mediawiki" ); - $conf->DBcataloged = importPost( "DBcataloged", "cataloged" ); - - $conf->ShellLocale = getShellLocale( $conf->LanguageCode ); - -/* Check for validity */ -$errs = array(); - -if( preg_match( '/^$|^mediawiki$|#/i', $conf->Sitename ) ) { - $errs["Sitename"] = "Must not be blank or \"MediaWiki\" and may not contain \"#\""; -} -if( $conf->DBuser == "" ) { - $errs["DBuser"] = "Must not be blank"; -} -if( ($conf->DBtype == 'mysql') && (strlen($conf->DBuser) > 16) ) { - $errs["DBuser"] = "Username too long"; -} -if( $conf->DBpassword == "" && $conf->DBtype != "postgres" ) { - $errs["DBpassword"] = "Must not be blank"; -} -if( $conf->DBpassword != $conf->DBpassword2 ) { - $errs["DBpassword2"] = "Passwords don't match!"; -} -if( !preg_match( '/^[A-Za-z_0-9]*$/', $conf->DBprefix ) ) { - $errs["DBprefix"] = "Invalid table prefix"; -} else { - untaint( $conf->DBprefix, TC_MYSQL ); -} - -error_reporting( E_ALL ); - -/** - * Initialise $wgLang and $wgContLang to something so we can - * call case-folding methods. Per Brion, this is English for - * now, although we could be clever and initialise to the - * user-selected language. - */ -$wgContLang = Language::factory( 'en' ); -$wgLang = $wgContLang; - -/** - * We're messing about with users, so we need a stub - * authentication plugin... - */ -$wgAuth = new AuthPlugin(); - -/** - * Validate the initial administrator account; username, - * password checks, etc. - */ -if( $conf->SysopName ) { - # Check that the user can be created - $u = User::newFromName( $conf->SysopName ); - if( is_a($u, 'User') ) { // please do not use instanceof, it breaks PHP4 - # Various password checks - if( $conf->SysopPass != '' ) { - if( $conf->SysopPass == $conf->SysopPass2 ) { - if( !$u->isValidPassword( $conf->SysopPass ) ) { - $errs['SysopPass'] = "Bad password"; - } - } else { - $errs['SysopPass2'] = "Passwords don't match"; - } - } else { - $errs['SysopPass'] = "Cannot be blank"; - } - unset( $u ); - } else { - $errs['SysopName'] = "Bad username"; - } -} - -$conf->License = importRequest( "License", "none" ); -if( $conf->License == "gfdl1_2" ) { - $conf->RightsUrl = "http://www.gnu.org/licenses/old-licenses/fdl-1.2.txt"; - $conf->RightsText = "GNU Free Documentation License 1.2"; - $conf->RightsCode = "gfdl1_2"; - $conf->RightsIcon = '${wgScriptPath}/skins/common/images/gnu-fdl.png'; -} elseif( $conf->License == "gfdl1_3" ) { - $conf->RightsUrl = "http://www.gnu.org/copyleft/fdl.html"; - $conf->RightsText = "GNU Free Documentation License 1.3"; - $conf->RightsCode = "gfdl1_3"; - $conf->RightsIcon = '${wgScriptPath}/skins/common/images/gnu-fdl.png'; -} elseif( $conf->License == "none" ) { - $conf->RightsUrl = $conf->RightsText = $conf->RightsCode = $conf->RightsIcon = ""; -} elseif( $conf->License == "pd" ) { - $conf->RightsUrl = "http://creativecommons.org/licenses/publicdomain/"; - $conf->RightsText = "Public Domain"; - $conf->RightsCode = "pd"; - $conf->RightsIcon = '${wgScriptPath}/skins/common/images/public-domain.png'; -} else { - $conf->RightsUrl = importRequest( "RightsUrl", "" ); - $conf->RightsText = importRequest( "RightsText", "" ); - $conf->RightsCode = importRequest( "RightsCode", "" ); - $conf->RightsIcon = importRequest( "RightsIcon", "" ); -} - -$conf->Shm = importRequest( "Shm", "none" ); -$conf->MCServers = importRequest( "MCServers" ); - -/* Test memcached servers */ - -if ( $conf->Shm == 'memcached' && $conf->MCServers ) { - $conf->MCServerArray = wfArrayMap( 'trim', explode( ',', $conf->MCServers ) ); - foreach ( $conf->MCServerArray as $server ) { - $error = testMemcachedServer( $server ); - if ( $error ) { - $errs["MCServers"] = $error; - break; - } - } -} else if ( $conf->Shm == 'memcached' ) { - $errs["MCServers"] = "Please specify at least one server if you wish to use memcached"; -} - -/* default values for installation */ -$conf->Email = importRequest("Email", "email_enabled"); -$conf->Emailuser = importRequest("Emailuser", "emailuser_enabled"); -$conf->Enotif = importRequest("Enotif", "enotif_allpages"); -$conf->Eauthent = importRequest("Eauthent", "eauthent_enabled"); - -if( $conf->posted && ( 0 == count( $errs ) ) ) { - do { /* So we can 'continue' to end prematurely */ - $conf->Root = ($conf->RootPW != ""); - - /* Load up the settings and get installin' */ - $local = writeLocalSettings( $conf ); - echo "
    • \n"; - echo "

      Generating configuration file...

      \n"; - echo "
    • \n"; - - $wgCommandLineMode = false; - chdir( ".." ); - $ok = eval( $local ); - if( $ok === false ) { - dieout( "

      Errors in generated configuration; " . - "most likely due to a bug in the installer... " . - "Config file was:

      " . - "
      " .
      -				htmlspecialchars( $local ) .
      -				"
      " ); - } - $conf->DBtypename = ''; - foreach (array_keys($ourdb) as $db) { - if ($conf->DBtype === $db) - $conf->DBtypename = $ourdb[$db]['fullname']; - } - if ( ! strlen($conf->DBtype)) { - $errs["DBpicktype"] = "Please choose a database type"; - continue; - } - - if (! $conf->DBtypename) { - $errs["DBtype"] = "Unknown database type '$conf->DBtype'"; - continue; - } - print "
    • Database type: " . htmlspecialchars( $conf->DBtypename ) . "
    • \n"; - $dbclass = 'Database'.ucfirst($conf->DBtype); - $wgDBtype = $conf->DBtype; - $wgDBadminuser = "root"; - $wgDBadminpassword = $conf->RootPW; - - ## Mysql specific: - $wgDBprefix = $conf->DBprefix; - - ## Postgres specific: - $wgDBport = $conf->DBport; - $wgDBmwschema = $conf->DBmwschema; - $wgDBts2schema = $conf->DBts2schema; - - if( $conf->DBprefix2 != '' ) { - // For MSSQL - $wgDBprefix = $conf->DBprefix2; - } - - ## DB2 specific: - $wgDBcataloged = $conf->DBcataloged; - - $wgCommandLineMode = true; - if (! defined ( 'STDERR' ) ) - define( 'STDERR', fopen("php://stderr", "wb")); - $wgUseDatabaseMessages = false; /* FIXME: For database failure */ - require_once( "$IP/includes/Setup.php" ); - chdir( "config" ); - - $wgTitle = Title::newFromText( "Installation script" ); - error_reporting( E_ALL ); - print "
    • Loading class: " . htmlspecialchars( $dbclass ) . "
    • \n"; - if ( $conf->DBtype != 'sqlite' ) { - $dbc = new $dbclass; - } - - if( $conf->DBtype == 'mysql' ) { - $mysqlOldClient = version_compare( mysql_get_client_info(), "4.1.0", "lt" ); - if( $mysqlOldClient ) { - print "
    • PHP is linked with old MySQL client libraries. If you are - using a MySQL 4.1 server and have problems connecting to the database, - see http://dev.mysql.com/doc/mysql/en/old-client.html for help.
    • \n"; - } - $ok = true; # Let's be optimistic - - # Decide if we're going to use the superuser or the regular database user - $conf->Root = $useRoot; - if( $conf->Root ) { - $db_user = $conf->RootUser; - $db_pass = $conf->RootPW; - } else { - $db_user = $wgDBuser; - $db_pass = $wgDBpassword; - } - - # Attempt to connect - echo( "
    • Attempting to connect to database server as " . htmlspecialchars( $db_user ) . "..." ); - $wgDatabase = Database::newFromParams( $wgDBserver, $db_user, $db_pass, '', 1 ); - - # Check the connection and respond to errors - if( $wgDatabase->isOpen() ) { - # Seems OK - $ok = true; - $wgDBadminuser = $db_user; - $wgDBadminpassword = $db_pass; - echo( "success.
    • \n" ); - $wgDatabase->ignoreErrors( true ); - $myver = $wgDatabase->getServerVersion(); - } else { - # There were errors, report them and back out - $ok = false; - $errno = mysql_errno(); - $errtx = htmlspecialchars( mysql_error() ); - switch( $errno ) { - case 1045: - case 2000: - echo( "failed due to authentication errors. Check passwords." ); - if( $conf->Root ) { - # The superuser details are wrong - $errs["RootUser"] = "Check username"; - $errs["RootPW"] = "and password"; - } else { - # The regular user details are wrong - $errs["DBuser"] = "Check username"; - $errs["DBpassword"] = "and password"; - } - break; - case 2002: - case 2003: - default: - # General connection problem - echo( htmlspecialchars( "failed with error [$errno] $errtx." ) . "\n" ); - $errs["DBserver"] = "Connection failed"; - break; - } # switch - } #conn. att. - - if( !$ok ) { continue; } - } - else if( $conf->DBtype == 'ibm_db2' ) { - if( $useRoot ) { - $db_user = $conf->RootUser; - $db_pass = $conf->RootPW; - } else { - $db_user = $wgDBuser; - $db_pass = $wgDBpassword; - } - - echo( "
    • Attempting to connect to database \"" . htmlspecialchars( $wgDBname ) . - "\" as \"" . htmlspecialchars( $db_user ) . "\"..." ); - $wgDatabase = $dbc->newFromParams($wgDBserver, $db_user, $db_pass, $wgDBname, 1); - if (!$wgDatabase->isOpen()) { - print " error: " . htmlspecialchars( $wgDatabase->lastError() ) . "
    • \n"; - } else { - $myver = $wgDatabase->getServerVersion(); - } - if (is_callable(array($wgDatabase, 'initial_setup'))) $wgDatabase->initial_setup('', $wgDBname); - - } elseif ( $conf->DBtype == 'sqlite' ) { - if ("$wgSQLiteDataDir" == '') { - $wgSQLiteDataDir = dirname($_SERVER['DOCUMENT_ROOT']).'/data'; - } - echo "
    • Attempting to connect to SQLite database at \"" . - htmlspecialchars( $wgSQLiteDataDir ) . "\""; - if ( !is_dir( $wgSQLiteDataDir ) ) { - if ( is_writable( dirname( $wgSQLiteDataDir ) ) ) { - $ok = wfMkdirParents( $wgSQLiteDataDir, $wgSQLiteDataDirMode ); - } else { - $ok = false; - } - if ( !$ok ) { - echo ": cannot create data directory
    • "; - $errs['SQLiteDataDir'] = 'Enter a valid data directory'; - continue; - } - } - if ( !is_writable( $wgSQLiteDataDir ) ) { - echo ": data directory not writable"; - $errs['SQLiteDataDir'] = 'Enter a writable data directory'; - continue; - } - $dataFile = "$wgSQLiteDataDir/$wgDBname.sqlite"; - if ( file_exists( $dataFile ) && !is_writable( $dataFile ) ) { - echo ": data file not writable"; - $errs['SQLiteDataDir'] = "$wgDBname.sqlite is not writable"; - continue; - } - $wgDatabase = new DatabaseSqlite( false, false, false, $wgDBname, 1 ); - if (!$wgDatabase->isOpen()) { - print ": error: " . htmlspecialchars( $wgDatabase->lastError() ) . "\n"; - $errs['SQLiteDataDir'] = 'Could not connect to database'; - continue; - } else { - $myver = $wgDatabase->getServerVersion(); - } - if (is_callable(array($wgDatabase, 'initial_setup'))) $wgDatabase->initial_setup('', $wgDBname); - echo "ok\n"; - } else { # not mysql - error_reporting( E_ALL ); - $wgSuperUser = ''; - ## Possible connect as a superuser - // Changed !mysql to postgres check since it seems to only apply to postgres - if( $useRoot && $conf->DBtype == 'postgres' ) { - $wgDBsuperuser = $conf->RootUser; - echo( "
    • Attempting to connect to database \"postgres\" as superuser \"" . - htmlspecialchars( $wgDBsuperuser ) . "\"..." ); - $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBsuperuser, $conf->RootPW, "postgres", 1); - if (!$wgDatabase->isOpen()) { - print " error: " . htmlspecialchars( $wgDatabase->lastError() ) . "
    • \n"; - $errs["DBserver"] = "Could not connect to database as superuser"; - $errs["RootUser"] = "Check username"; - $errs["RootPW"] = "and password"; - continue; - } - $wgDatabase->initial_setup($conf->RootPW, 'postgres'); - } - echo( "
    • Attempting to connect to database \"" . htmlspecialchars( $wgDBname ) . - "\" as \"" . htmlspecialchars( $wgDBuser ) . "\"..." ); - $wgDatabase = $dbc->newFromParams($wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1); - if (!$wgDatabase->isOpen()) { - print " error: " . htmlspecialchars( $wgDatabase->lastError() ) . "
    • \n"; - } else { - $myver = $wgDatabase->getServerVersion(); - } - if (is_callable(array($wgDatabase, 'initial_setup'))) $wgDatabase->initial_setup('', $wgDBname); - } - - if ( !$wgDatabase->isOpen() ) { - $errs["DBserver"] = "Couldn't connect to database"; - continue; - } - - print "
    • Connected to " . htmlspecialchars( "{$conf->DBtype} $myver" ); - if ($conf->DBtype == 'mysql') { - if( version_compare( $myver, "4.0.14" ) < 0 ) { - print "
    • \n"; - dieout( "-- mysql 4.0.14 or later required. Aborting." ); - } - $mysqlNewAuth = version_compare( $myver, "4.1.0", "ge" ); - if( $mysqlNewAuth && $mysqlOldClient ) { - print "; You are using MySQL 4.1 server, but PHP is linked - to old client libraries; if you have trouble with authentication, see - http://dev.mysql.com/doc/mysql/en/old-client.html for help."; - } - if( $wgDBmysql5 ) { - if( $mysqlNewAuth ) { - print "; enabling MySQL 4.1/5.0 charset mode"; - } else { - print "; MySQL 4.1/5.0 charset mode enabled, - but older version detected; will likely fail."; - } - } - print "\n"; - - @$sel = $wgDatabase->selectDB( $wgDBname ); - if( $sel ) { - print "
    • Database " . htmlspecialchars( $wgDBname ) . " exists
    • \n"; - } else { - $err = mysql_errno(); - $databaseSafe = htmlspecialchars( $wgDBname ); - if( $err == 1102 /* Invalid database name */ ) { - print "
      • {$databaseSafe} is not a valid database name.
      "; - continue; - } elseif( $err != 1049 /* Database doesn't exist */ ) { - print "
      • Error selecting database {$databaseSafe}: {$err} "; - print htmlspecialchars( mysql_error() ) . "
      "; - continue; - } - print "
    • Attempting to create database...
    • "; - $res = $wgDatabase->query( "CREATE DATABASE `$wgDBname`" ); - if( !$res ) { - print "
    • Couldn't create database " . - htmlspecialchars( $wgDBname ) . - "; try with root access or check your username/pass.
    • \n"; - $errs["RootPW"] = "<- Enter"; - continue; - } - print "
    • Created database " . htmlspecialchars( $wgDBname ) . "
    • \n"; - } - $wgDatabase->selectDB( $wgDBname ); - } - else if ($conf->DBtype == 'postgres') { - if( version_compare( $myver, "8.0" ) < 0 ) { - dieout( "Postgres 8.0 or later is required. Aborting." ); - } - } - - if( $wgDatabase->tableExists( "cur" ) || $wgDatabase->tableExists( "revision" ) ) { - print "
    • There are already MediaWiki tables in this database. Checking if updates are needed...
    • \n"; - - if ( $conf->DBtype == 'mysql') { - # Determine existing default character set - if ( $wgDatabase->tableExists( "revision" ) ) { - $revision = $wgDatabase->escapeLike( $conf->DBprefix . 'revision' ); - $res = $wgDatabase->query( "SHOW TABLE STATUS LIKE '$revision'" ); - $row = $wgDatabase->fetchObject( $res ); - if ( !$row ) { - echo "
    • SHOW TABLE STATUS query failed!
    • \n"; - $existingSchema = false; - $existingEngine = false; - } else { - if ( preg_match( '/^latin1/', $row->Collation ) ) { - $existingSchema = 'mysql4'; - } elseif ( preg_match( '/^utf8/', $row->Collation ) ) { - $existingSchema = 'mysql5'; - } elseif ( preg_match( '/^binary/', $row->Collation ) ) { - $existingSchema = 'mysql5-binary'; - } else { - $existingSchema = false; - echo "
    • Warning: Unrecognised existing collation
    • \n"; - } - if ( isset( $row->Engine ) ) { - $existingEngine = $row->Engine; - } else { - $existingEngine = $row->Type; - } - } - if ( $existingSchema && $existingSchema != $conf->DBschema ) { - $encExisting = htmlspecialchars( $existingSchema ); - $encRequested = htmlspecialchars( $conf->DBschema ); - print "
    • Warning: you requested the $encRequested schema, " . - "but the existing database has the $encExisting schema. This upgrade script ". - "can't convert it, so it will remain $encExisting.
    • \n"; - $conf->setSchema( $existingSchema, $conf->DBengine ); - } - if ( $existingEngine && $existingEngine != $conf->DBengine ) { - $encExisting = htmlspecialchars( $existingEngine ); - $encRequested = htmlspecialchars( $conf->DBengine ); - print "
    • Warning: you requested the $encRequested storage " . - "engine, but the existing database uses the $encExisting engine. This upgrade " . - "script can't convert it, so it will remain $encExisting.
    • \n"; - $conf->setSchema( $conf->DBschema, $existingEngine ); - } - } - - # Create user if required - if ( $conf->Root ) { - $conn = $dbc->newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); - if ( $conn->isOpen() ) { - print "
    • DB user account ok
    • \n"; - $conn->close(); - } else { - print "
    • Granting user permissions..."; - if( $mysqlOldClient && $mysqlNewAuth ) { - print " If the next step fails, see http://dev.mysql.com/doc/mysql/en/old-client.html for help."; - } - print "
    • \n"; - dbsource( "../maintenance/users.sql", $wgDatabase ); - } - } - } - print "
    \n";
    -			chdir( ".." );
    -			flush();
    -			do_all_updates();
    -			chdir( "config" );
    -			print "
    \n"; - print "
    • Finished update checks.
    • \n"; - } else { - # Determine available storage engines if possible - if ( $conf->DBtype == 'mysql' && version_compare( $myver, "4.1.2", "ge" ) ) { - $res = $wgDatabase->query( 'SHOW ENGINES' ); - $found = false; - while ( $row = $wgDatabase->fetchObject( $res ) ) { - if ( $row->Engine == $conf->DBengine ) { - $found = true; - break; - } - } - $wgDatabase->freeResult( $res ); - if ( !$found && $conf->DBengine != 'MyISAM' ) { - echo "
    • Warning: " . htmlspecialchars( $conf->DBengine ) . - " storage engine not available, " . - "using MyISAM instead
    • \n"; - $conf->setSchema( $conf->DBschema, 'MyISAM' ); - } - } - - # FIXME: Check for errors - print "
    • Creating tables..."; - if ($conf->DBtype == 'mysql') { - dbsource( "../maintenance/tables.sql", $wgDatabase ); - dbsource( "../maintenance/interwiki.sql", $wgDatabase ); - } elseif (is_callable(array($wgDatabase, 'setup_database'))) { - $wgDatabase->setup_database(); - } - else { - $errs["DBtype"] = "Do not know how to handle database type '$conf->DBtype'"; - continue; - } - - print " done.
    • \n"; - - print "
    • Initializing statistics...
    • \n"; - $wgDatabase->insert( 'site_stats', - array ( 'ss_row_id' => 1, - 'ss_total_views' => 0, - 'ss_total_edits' => 1, # Main page first edit - 'ss_good_articles' => 0, # Main page is not a good article - no internal link - 'ss_total_pages' => 1, # Main page - 'ss_users' => $conf->SysopName ? 1 : 0, # Sysop account, if created - 'ss_admins' => $conf->SysopName ? 1 : 0, # Sysop account, if created - 'ss_images' => 0 ) ); - - # Set up the "regular user" account *if we can, and if we need to* - if( $conf->Root and $conf->DBtype == 'mysql') { - # See if we need to - $wgDatabase2 = $dbc->newFromParams( $wgDBserver, $wgDBuser, $wgDBpassword, $wgDBname, 1 ); - if( $wgDatabase2->isOpen() ) { - # Nope, just close the test connection and continue - $wgDatabase2->close(); - echo( "
    • User " . htmlspecialchars( $wgDBuser ) . " exists. Skipping grants.
    • \n" ); - } else { - # Yes, so run the grants - echo( "
    • " . htmlspecialchars( "Granting user permissions to $wgDBuser on $wgDBname..." ) ); - dbsource( "../maintenance/users.sql", $wgDatabase ); - echo( "success.
    • \n" ); - } - } - - if( $conf->SysopName ) { - $u = User::newFromName( $conf->getSysopName() ); - if ( !$u ) { - print "
    • Warning: Skipped sysop account creation - invalid username!
    • \n"; - } - else if ( 0 == $u->idForName() ) { - $u->addToDatabase(); - $u->setPassword( $conf->getSysopPass() ); - $u->saveSettings(); - - $u->addGroup( "sysop" ); - $u->addGroup( "bureaucrat" ); - - print "
    • Created sysop account " . - htmlspecialchars( $conf->SysopName ) . ".
    • \n"; - } else { - print "
    • Could not create user - already exists!
    • \n"; - } - } else { - print "
    • Skipped sysop account creation, no name given.
    • \n"; - } - - $titleobj = Title::newFromText( wfMsgNoDB( "mainpage" ) ); - $article = new Article( $titleobj ); - $newid = $article->insertOn( $wgDatabase ); - $revision = new Revision( array( - 'page' => $newid, - 'text' => wfMsg( 'mainpagetext' ) . "\n\n" . wfMsgNoTrans( 'mainpagedocfooter' ), - 'comment' => '', - 'user' => 0, - 'user_text' => 'MediaWiki default', - ) ); - $revid = $revision->insertOn( $wgDatabase ); - $article->updateRevisionOn( $wgDatabase, $revision ); - } - // Now that all database work is done, make sure everything is committed - $wgDatabase->commit(); - - /* Write out the config file now that all is well */ - print "
    • \n"; - print "

      Creating LocalSettings.php...

      \n\n"; - $localSettings = "<" . "?php$endl$local"; - // Fix up a common line-ending problem (due to CVS on Windows) - $localSettings = str_replace( "\r\n", "\n", $localSettings ); - $f = fopen( "LocalSettings.php", 'xt' ); - - if( $f == false ) { - print( "
    • \n" ); - dieout( "

      Couldn't write out LocalSettings.php. Check that the directory permissions are correct and that there isn't already a file of that name here...

      \n" . - "

      Here's the file that would have been written, try to paste it into place manually:

      \n" . - "
      \n" . htmlspecialchars( $localSettings ) . "
      \n" ); - } - if(fwrite( $f, $localSettings ) ) { - fclose( $f ); - print "
      \n"; - writeSuccessMessage(); - print "\n"; - } else { - fclose( $f ); - dieout( "

      An error occured while writing the config/LocalSettings.php file. Check user rights and disk space then try again.

      \n" ); - } - - } while( false ); -} - -print "
    \n"; -$mainListOpened = false; - -if( count( $errs ) ) { - /* Display options form */ - - if( $conf->posted ) { - echo "

    Something's not quite right yet; make sure everything below is filled out correctly.

    \n"; - } -?> - -
    - -

    Site config

    - -
    -
    - -
    -

    - Preferably a short word without punctuation, i.e. "Wikipedia".
    - Will appear as the namespace name for "meta" pages, and throughout the interface. -

    -
    -

    - Displayed to users in some error messages, used as the return address for password reminders, and used as the default sender address of e-mail notifications. -

    - -
    - - -
    -

    - Select the language for your wiki's interface. Some localizations aren't fully complete. Unicode (UTF-8) is used for all localizations. -

    - -
    - - -
      -
    • -
    • -
    • -
    • -
    • ScriptPath}/config/$script?License=cc&RightsUrl=[license_url]&RightsText=[license_name]&RightsCode=[license_code]&RightsIcon=[license_button]" ); - $icon = urlencode( "$wgServer$wgUploadPath/wiki.png" ); - $ccApp = htmlspecialchars( "http://creativecommons.org/license/?partner=$partner&exit_url=$exit&partner_icon_url=$icon" ); - print "choose"; - if( $conf->License == "cc" ) { ?> -
        -
      • RightsIcon ) . "\" alt='(Creative Commons icon)' />", "hidden" ); ?>
      • -
      • RightsText ), "hidden" ); ?>
      • -
      • RightsCode ), "hidden" ); ?>
      • -
      • RightsUrl ) . "\">" . htmlspecialchars( $conf->RightsUrl ) . "", "hidden" ); ?>
      • -
      - -
    • -
    -
    -

    - A notice, icon, and machine-readable copyright metadata will be displayed for the license you pick. -

    - - -
    - -
    -
    - -
    -
    - -
    -

    - An admin can lock/delete pages, block users from editing, and do other maintenance tasks.
    - A new account will be added only when creating a new wiki database. -

    - The password cannot be the same as the username. -

    - -
    - - -
      -
    • - turck ) { - echo "
    • "; - aField( $conf, "Shm", "Turck MMCache", "radio", "turck" ); - echo "
    • \n"; - } - if( $conf->xcache ) { - echo "
    • "; - aField( $conf, 'Shm', 'XCache', 'radio', 'xcache' ); - echo "
    • \n"; - } - if ( $conf->apc ) { - echo "
    • "; - aField( $conf, "Shm", "APC", "radio", "apc" ); - echo "
    • \n"; - } - if ( $conf->eaccel ) { - echo "
    • "; - aField( $conf, "Shm", "eAccelerator", "radio", "eaccel" ); - echo "
    • \n"; - } - if ( $conf->dba ) { - echo "
    • "; - aField( $conf, "Shm", "DBA (not recommended)", "radio", "dba" ); - echo "
    • "; - } - ?> -
    • -
    -
    -
    -

    - An object caching system such as memcached will provide a significant performance boost, - but needs to be installed. Provide the server addresses and ports in a comma-separated list. -

    - MediaWiki can also detect and support eAccelerator, Turck MMCache, APC, and XCache, but - these should not be used if the wiki will be running on multiple application servers. -

    - DBA (Berkeley-style DB) is generally slower than using no cache at all, and is only - recommended for testing. -

    -
    - -

    E-mail, e-mail notification and authentication setup

    - -
    -
    - -
      -
    • -
    • -
    -
    -

    - Use this to disable all e-mail functions (password reminders, user-to-user e-mail, and e-mail notifications) - if sending mail doesn't work on your server. -

    - -
    - -
      -
    • -
    • -
    -
    -

    - The user-to-user e-mail feature (Special:Emailuser) lets the wiki act as a relay to allow users to exchange e-mail without publicly advertising their e-mail address. -

    -
    - -
      -
    • -
    • -
    • -
    -
    -
    -

    - For this feature to work, an e-mail address must be present for the user account, and the notification - options in the user's preferences must be enabled. Also note the - authentication option below. When testing the feature, keep in mind that your own changes will never trigger notifications to be sent to yourself.

    - -

    There are additional options for fine tuning in /includes/DefaultSettings.php; copy these to your LocalSettings.php and edit them there to change them.

    -
    - -
    - -
      -
    • -
    • -
    -
    -
    -

    If this option is enabled, users have to confirm their e-mail address using a magic link sent to them whenever they set or change it, and only authenticated e-mail addresses can receive mails from other users and/or - change notification mails. Setting this option is recommended for public wikis because of potential abuse of the e-mail features above.

    -
    - -
    - -

    Database config

    - -
    -
    - -" . htmlspecialchars( $errs['DBpicktype'] ) . "\n"; - } -?> -
    -
    - -
    - -
    -

    - If your database server isn't on your web server, enter the name or IP address here. -

    - -
    -
    -
    -
    -

    - If you only have a single user account and database available, - enter those here. If you have database root access (see below) - you can specify new accounts/databases to be created. This account - will not be created if it pre-exists. If this is the case, ensure that it - has SELECT, INSERT, UPDATE, and DELETE permissions on the MediaWiki database. -

    - -
    - - checked="checked" /> -   -
    -
    -
    - -

    - If the database user specified above does not exist, or does not have access to create - the database (if needed) or tables within it, please check the box and provide details - of a superuser account, such as root, which does. -

    - - -
    -
    -

    If you need to share one database between multiple wikis, or - between MediaWiki and another web application, you may choose to - add a prefix to all the table names to avoid conflicts.

    - -

    Avoid exotic characters; something like mw_ is good.

    -
    - -
    -
    Select one:
    -
      -
    • -
    • -
    -
    -

    - InnoDB is best for public web installations, since it has good concurrency - support. MyISAM may be faster in single-user installations. MyISAM databases - tend to get corrupted more often than InnoDB databases. -

    -
    -
    Select one:
    -
      -
    • -
    • -
    • -
    -
    -

    - This option is ignored on upgrade, the same character set will be kept. -

    - WARNING: If you use backwards-compatible UTF-8 on MySQL 4.1+, and subsequently back up the database with mysqldump, it may destroy all non-ASCII characters, irreversibly corrupting your backups!. -

    - In binary mode, MediaWiki stores UTF-8 text to the database in binary fields. This is more efficient than MySQL's UTF-8 mode, and allows you to use the full range of Unicode characters. In UTF-8 mode, MySQL will know what character set your data is in, and can present and convert it appropriately, but it won't let you store characters above the Basic Multilingual Plane. -

    -
    - - -
    -
    -
    -
    -

    The username specified above (at "DB username") will have its search path set to the above schemas, - so it is recommended that you create a new user. The above schemas are generally correct: - only change them if you are sure you need to.

    -
    - - - -
    - NOTE: SQLite only uses the Database name setting above, the user, password and root settings are ignored. -
    -
    -
    -

    SQLite stores table data into files in the filesystem. - If you do not provide an explicit path, a "data" directory in - the parent of your document root will be used.

    - -

    This directory must exist and be writable by the web server.

    -
    - - - -
    -
    -

    If you need to share one database between multiple wikis, or - between MediaWiki and another web application, you may choose to - add a prefix to all the table names to avoid conflicts.

    - -

    Avoid exotic characters; something like mw_ is good.

    -
    - - - -
    -
    -
    Select one:
    -
      -
    • -
    • -
    -
    -

    If you need to share one database between multiple wikis, or - between MediaWiki and another web application, you may specify - a different schema to avoid conflicts.

    -
    - - -
    - - -
    - - - - -

    Installation successful!

    -

    To complete the installation, please do the following: -

      -
    1. Download config/LocalSettings.php with your FTP client or file manager
    2. -
    3. Upload it to the parent directory
    4. -
    5. Delete config/LocalSettings.php
    6. -
    7. Start using your wiki! -
    -

    If you are in a shared hosting environment, do not just move LocalSettings.php -remotely. LocalSettings.php is currently owned by the user your webserver is running under, -which means that anyone on the same server can read your database password! Downloading -it and uploading it again will hopefully change the ownership to a user ID specific to you.

    - -EOT; - } else { - echo << -

    -Installation successful! -Move the config/LocalSettings.php file to the parent directory, then follow - this link to your wiki.

    -

    You should change file permissions for LocalSettings.php as required to -prevent other users on the server reading passwords and altering configuration data.

    - -EOT; - } -} - - -function escapePhpString( $string ) { - if ( is_array( $string ) || is_object( $string ) ) { - return false; - } - return strtr( $string, - array( - "\n" => "\\n", - "\r" => "\\r", - "\t" => "\\t", - "\\" => "\\\\", - "\$" => "\\\$", - "\"" => "\\\"" - )); -} - -function writeLocalSettings( $conf ) { - $conf->PasswordSender = $conf->EmergencyContact; - $magic = ($conf->ImageMagick ? "" : "# "); - $convert = ($conf->ImageMagick ? $conf->ImageMagick : "/usr/bin/convert" ); - $rights = ($conf->RightsUrl) ? "" : "# "; - $hashedUploads = $conf->safeMode ? '' : '# '; - - if ( $conf->ShellLocale ) { - $locale = ''; - } else { - $locale = '# '; - $conf->ShellLocale = 'en_US.UTF-8'; - } - - switch ( $conf->Shm ) { - case 'memcached': - $cacheType = 'CACHE_MEMCACHED'; - $mcservers = var_export( $conf->MCServerArray, true ); - break; - case 'turck': - case 'xcache': - case 'apc': - case 'eaccel': - $cacheType = 'CACHE_ACCEL'; - $mcservers = 'array()'; - break; - case 'dba': - $cacheType = 'CACHE_DBA'; - $mcservers = 'array()'; - break; - default: - $cacheType = 'CACHE_NONE'; - $mcservers = 'array()'; - } - - if ( $conf->Email == 'email_enabled' ) { - $enableemail = 'true'; - $enableuseremail = ( $conf->Emailuser == 'emailuser_enabled' ) ? 'true' : 'false' ; - $eauthent = ( $conf->Eauthent == 'eauthent_enabled' ) ? 'true' : 'false' ; - switch ( $conf->Enotif ) { - case 'enotif_usertalk': - $enotifusertalk = 'true'; - $enotifwatchlist = 'false'; - break; - case 'enotif_allpages': - $enotifusertalk = 'true'; - $enotifwatchlist = 'true'; - break; - default: - $enotifusertalk = 'false'; - $enotifwatchlist = 'false'; - } - } else { - $enableuseremail = 'false'; - $enableemail = 'false'; - $eauthent = 'false'; - $enotifusertalk = 'false'; - $enotifwatchlist = 'false'; - } - - $file = @fopen( "/dev/urandom", "r" ); - if ( $file ) { - $secretKey = bin2hex( fread( $file, 32 ) ); - fclose( $file ); - } else { - $secretKey = ""; - for ( $i=0; $i<8; $i++ ) { - $secretKey .= dechex(mt_rand(0, 0x7fffffff)); - } - print "
  • Warning: \$wgSecretKey key is insecure, generated with mt_rand(). Consider changing it manually.
  • \n"; - } - - # Add slashes to strings for double quoting - $slconf = wfArrayMap( "escapePhpString", get_object_vars( $conf ) ); - if( $conf->License == 'gfdl1_2' || $conf->License == 'pd' || $conf->License == 'gfdl1_3' ) { - # Needs literal string interpolation for the current style path - $slconf['RightsIcon'] = $conf->RightsIcon; - } - - if( $conf->DBtype == 'mysql' ) { - $dbsettings = -"# MySQL specific settings -\$wgDBprefix = \"{$slconf['DBprefix']}\"; - -# MySQL table options to use during installation or update -\$wgDBTableOptions = \"{$slconf['DBTableOptions']}\"; - -# Experimental charset support for MySQL 4.1/5.0. -\$wgDBmysql5 = {$conf->DBmysql5};"; - } elseif( $conf->DBtype == 'postgres' ) { - $dbsettings = -"# Postgres specific settings -\$wgDBport = \"{$slconf['DBport']}\"; -\$wgDBmwschema = \"{$slconf['DBmwschema']}\"; -\$wgDBts2schema = \"{$slconf['DBts2schema']}\";"; - } elseif( $conf->DBtype == 'sqlite' ) { - $dbsettings = -"# SQLite-specific settings -\$wgSQLiteDataDir = \"{$slconf['SQLiteDataDir']}\";"; - } elseif( $conf->DBtype == 'mssql' ) { - $dbsettings = -"# MSSQL specific settings -\$wgDBprefix = \"{$slconf['DBprefix2']}\";"; - } elseif( $conf->DBtype == 'ibm_db2' ) { - $dbsettings = -"# DB2 specific settings -\$wgDBport_db2 = \"{$slconf['DBport_db2']}\"; -\$wgDBmwschema = \"{$slconf['DBmwschema']}\"; -\$wgDBcataloged = \"{$slconf['DBcataloged']}\";"; - } else { - // ummm... :D - $dbsettings = ''; - } - - - $localsettings = " -# This file was automatically generated by the MediaWiki installer. -# If you make manual changes, please keep track in case you need to -# recreate them later. -# -# See includes/DefaultSettings.php for all configurable settings -# and their default values, but don't forget to make changes in _this_ -# file, not there. -# -# Further documentation for configuration settings may be found at: -# http://www.mediawiki.org/wiki/Manual:Configuration_settings - -# If you customize your file layout, set \$IP to the directory that contains -# the other MediaWiki files. It will be used as a base to locate files. -if( defined( 'MW_INSTALL_PATH' ) ) { - \$IP = MW_INSTALL_PATH; -} else { - \$IP = dirname( __FILE__ ); -} - -\$path = array( \$IP, \"\$IP/includes\", \"\$IP/languages\" ); -set_include_path( implode( PATH_SEPARATOR, \$path ) . PATH_SEPARATOR . get_include_path() ); - -require_once( \"\$IP/includes/DefaultSettings.php\" ); - -# If PHP's memory limit is very low, some operations may fail. -" . ($conf->raiseMemory ? '' : '# ' ) . "ini_set( 'memory_limit', '20M' );" . " - -if ( \$wgCommandLineMode ) { - if ( isset( \$_SERVER ) && array_key_exists( 'REQUEST_METHOD', \$_SERVER ) ) { - die( \"This script must be run from the command line\\n\" ); - } -} -## Uncomment this to disable output compression -# \$wgDisableOutputCompression = true; - -\$wgSitename = \"{$slconf['Sitename']}\"; - -## The URL base path to the directory containing the wiki; -## defaults for all runtime URL paths are based off of this. -## For more information on customizing the URLs please see: -## http://www.mediawiki.org/wiki/Manual:Short_URL -\$wgScriptPath = \"{$slconf['ScriptPath']}\"; -\$wgScriptExtension = \"{$slconf['ScriptExtension']}\"; - -## UPO means: this is also a user preference option - -\$wgEnableEmail = $enableemail; -\$wgEnableUserEmail = $enableuseremail; # UPO - -\$wgEmergencyContact = \"{$slconf['EmergencyContact']}\"; -\$wgPasswordSender = \"{$slconf['PasswordSender']}\"; - -\$wgEnotifUserTalk = $enotifusertalk; # UPO -\$wgEnotifWatchlist = $enotifwatchlist; # UPO -\$wgEmailAuthentication = $eauthent; - -## Database settings -\$wgDBtype = \"{$slconf['DBtype']}\"; -\$wgDBserver = \"{$slconf['DBserver']}\"; -\$wgDBname = \"{$slconf['DBname']}\"; -\$wgDBuser = \"{$slconf['DBuser']}\"; -\$wgDBpassword = \"{$slconf['DBpassword']}\"; - -{$dbsettings} - -## Shared memory settings -\$wgMainCacheType = $cacheType; -\$wgMemCachedServers = $mcservers; - -## To enable image uploads, make sure the 'images' directory -## is writable, then set this to true: -\$wgEnableUploads = false; -{$magic}\$wgUseImageMagick = true; -{$magic}\$wgImageMagickConvertCommand = \"{$convert}\"; - -## If you use ImageMagick (or any other shell command) on a -## Linux server, this will need to be set to the name of an -## available UTF-8 locale -{$locale}\$wgShellLocale = \"{$slconf['ShellLocale']}\"; - -## If you want to use image uploads under safe mode, -## create the directories images/archive, images/thumb and -## images/temp, and make them all writable. Then uncomment -## this, if it's not already uncommented: -{$hashedUploads}\$wgHashedUploadDirectory = false; - -## If you have the appropriate support software installed -## you can enable inline LaTeX equations: -\$wgUseTeX = false; - -\$wgLocalInterwiki = strtolower( \$wgSitename ); - -\$wgLanguageCode = \"{$slconf['LanguageCode']}\"; - -\$wgSecretKey = \"$secretKey\"; - -## Default skin: you can change the default skin. Use the internal symbolic -## names, ie 'standard', 'nostalgia', 'cologneblue', 'monobook': -\$wgDefaultSkin = 'monobook'; - -## For attaching licensing metadata to pages, and displaying an -## appropriate copyright notice / icon. GNU Free Documentation -## License and Creative Commons licenses are supported so far. -{$rights}\$wgEnableCreativeCommonsRdf = true; -\$wgRightsPage = \"\"; # Set to the title of a wiki page that describes your license/copyright -\$wgRightsUrl = \"{$slconf['RightsUrl']}\"; -\$wgRightsText = \"{$slconf['RightsText']}\"; -\$wgRightsIcon = \"{$slconf['RightsIcon']}\"; -# \$wgRightsCode = \"{$slconf['RightsCode']}\"; # Not yet used - -\$wgDiff3 = \"{$slconf['diff3']}\"; - -# When you make changes to this configuration file, this will make -# sure that cached pages are cleared. -\$wgCacheEpoch = max( \$wgCacheEpoch, gmdate( 'YmdHis', @filemtime( __FILE__ ) ) ); -"; ## End of setting the $localsettings string - - // Keep things in Unix line endings internally; - // the system will write out as local text type. - return str_replace( "\r\n", "\n", $localsettings ); -} - -function dieout( $text ) { - global $mainListOpened; - if( $mainListOpened ) echo( "" ); - if( $text != '' && substr( $text, 0, 2 ) != '$text

    \n"; - } else { - echo $text; - } - die( "\n\n\n\n\n\n\n" ); -} - -function importVar( &$var, $name, $default = "" ) { - if( isset( $var[$name] ) ) { - $retval = $var[$name]; - if ( get_magic_quotes_gpc() ) { - $retval = stripslashes( $retval ); - } - } else { - $retval = $default; - } - taint( $retval ); - return $retval; -} - -function importPost( $name, $default = "" ) { - return importVar( $_POST, $name, $default ); -} - -function importCheck( $name ) { - return isset( $_POST[$name] ); -} - -function importRequest( $name, $default = "" ) { - return importVar( $_REQUEST, $name, $default ); -} - -function aField( &$conf, $field, $text, $type = "text", $value = "", $onclick = '' ) { - static $radioCount = 0; - if( $type != "" ) { - $xtype = "type=\"$type\""; - } else { - $xtype = ""; - } - - $id = $field; - $nolabel = ($type == "radio") || ($type == "hidden"); - - if ($type == 'radio') - $id .= $radioCount++; - - if( !$nolabel ) { - echo ""; - } - - if( $type == "radio" && $value == $conf->$field ) { - $checked = "checked='checked'"; - } else { - $checked = ""; - } - echo "$field ); - } - - - echo "\" />"; - if( $nolabel ) { - echo ""; - } - - global $errs; - if(isset($errs[$field])) { - echo "" . htmlspecialchars( $errs[$field] ) . "\n"; - } -} - -function getLanguageList() { - global $wgLanguageNames, $IP; - if( !isset( $wgLanguageNames ) ) { - require_once( "$IP/languages/Names.php" ); - } - - $codes = array(); - - $d = opendir( "../languages/messages" ); - /* In case we are called from the root directory */ - if (!$d) - $d = opendir( "languages/messages"); - while( false !== ($f = readdir( $d ) ) ) { - $m = array(); - if( preg_match( '/Messages([A-Z][a-z_]+)\.php$/', $f, $m ) ) { - $code = str_replace( '_', '-', strtolower( $m[1] ) ); - if( isset( $wgLanguageNames[$code] ) ) { - $name = $code . ' - ' . $wgLanguageNames[$code]; - } else { - $name = $code; - } - $codes[$code] = $name; - } - } - closedir( $d ); - ksort( $codes ); - return $codes; -} - -#Check for location of an executable -# @param string $loc single location to check -# @param array $names filenames to check for. -# @param mixed $versioninfo array of details to use when checking version, use false for no version checking -function locate_executable($loc, $names, $versioninfo = false) { - if (!is_array($names)) - $names = array($names); - - foreach ($names as $name) { - $command = "$loc".DIRECTORY_SEPARATOR."$name"; - if (@file_exists($command)) { - if (!$versioninfo) - return $command; - - $file = str_replace('$1', $command, $versioninfo[0]); - if (strstr(`$file`, $versioninfo[1]) !== false) - return $command; - } - } - return false; -} - -# Test a memcached server -function testMemcachedServer( $server ) { - $hostport = explode(":", $server); - $errstr = false; - $fp = false; - if ( !function_exists( 'fsockopen' ) ) { - $errstr = "Can't connect to memcached, fsockopen() not present"; - } - if ( !$errstr && count( $hostport ) != 2 ) { - $errstr = 'Please specify host and port'; - } - if ( !$errstr ) { - list( $host, $port ) = $hostport; - $errno = 0; - $fsockerr = ''; - - $fp = @fsockopen( $host, $port, $errno, $fsockerr, 1.0 ); - if ( $fp === false ) { - $errstr = "Cannot connect to memcached on $host:$port : $fsockerr"; - } - } - if ( !$errstr ) { - $command = "version\r\n"; - $bytes = fwrite( $fp, $command ); - if ( $bytes != strlen( $command ) ) { - $errstr = "Cannot write to memcached socket on $host:$port"; - } - } - if ( !$errstr ) { - $expected = "VERSION "; - $response = fread( $fp, strlen( $expected ) ); - if ( $response != $expected ) { - $errstr = "Didn't get correct memcached response from $host:$port"; - } - } - if ( $fp ) { - fclose( $fp ); - } - if ( !$errstr ) { - echo "
  • Connected to memcached on " . htmlspecialchars( "$host:$port" ) ." successfully
  • "; - } - return $errstr; -} - -function database_picker($conf) { - global $ourdb; - print "\n"; - foreach(array_keys($ourdb) as $db) { - if ($ourdb[$db]['havedriver']) { - print "\t
  • "; - aField( $conf, "DBtype", $ourdb[$db]['fullname'], 'radio', $db, 'onclick'); - print "
  • \n"; - } - } - print "\n\t"; -} - -function database_switcher($db) { - global $ourdb; - $color = $ourdb[$db]['bgcolor']; - $full = $ourdb[$db]['fullname']; - print "
    $full specific options\n"; -} - -function printListItem( $item ) { - print "
  • $item
  • "; -} - -# Determine a suitable value for $wgShellLocale -function getShellLocale( $wikiLang ) { - # Give up now if we're in safe mode or open_basedir - # It's theoretically possible but tricky to work with - if ( wfIniGetBool( "safe_mode" ) || ini_get( 'open_basedir' ) ) { - return false; - } - - $os = php_uname( 's' ); - $supported = array( 'Linux', 'SunOS', 'HP-UX' ); # Tested these - if ( !in_array( $os, $supported ) ) { - return false; - } - - # Get a list of available locales - $lines = $ret = false; - exec( '/usr/bin/locale -a', $lines, $ret ); - if ( $ret ) { - return false; - } - - $lines = wfArrayMap( 'trim', $lines ); - $candidatesByLocale = array(); - $candidatesByLang = array(); - foreach ( $lines as $line ) { - if ( $line === '' ) { - continue; - } - if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) { - continue; - } - list( $all, $lang, $territory, $charset, $modifier ) = $m; - $candidatesByLocale[$m[0]] = $m; - $candidatesByLang[$lang][] = $m; - } - - # Try the current value of LANG - if ( isset( $candidatesByLocale[ getenv( 'LANG' ) ] ) ) { - return getenv( 'LANG' ); - } - - # Try the most common ones - $commonLocales = array( 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ); - foreach ( $commonLocales as $commonLocale ) { - if ( isset( $candidatesByLocale[$commonLocale] ) ) { - return $commonLocale; - } - } - - # Is there an available locale in the Wiki's language? - if ( isset( $candidatesByLang[$wikiLang] ) ) { - $m = reset( $candidatesByLang[$wikiLang] ); - return $m[0]; - } - - # Are there any at all? - if ( count( $candidatesByLocale ) ) { - $m = reset( $candidatesByLocale ); - return $m[0]; - } - - # Give up - return false; -} - -function wfArrayMap( $function, $input ) { - $ret = array_map( $function, $input ); - foreach ( $ret as $key => $value ) { - $taint = istainted( $input[$key] ); - if ( $taint ) { - taint( $ret[$key], $taint ); - } - } - return $ret; -} - -?> - -
    -
    -

    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. - or read it online

    -
    - - - - -
    - - -
    - -

    MediaWiki is Copyright © 2001-2009 by 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.

    -
    -
    - - - - - +// Isolate the rest of the code so this file can die out cleanly +// if we find we're running under PHP 4.x... We use PHP 5 syntax +// which doesn't parse under 4. +require( dirname( __FILE__ ) . "/Installer.php" ); diff --git a/config/index.php5 b/config/index.php5 index 1be08780..8e6ceda9 100644 --- a/config/index.php5 +++ b/config/index.php5 @@ -2,5 +2,3 @@ define('MW_INSTALL_PHP5_EXT', 1); require './index.php'; - -?> diff --git a/docs/design.txt b/docs/design.txt index d1904e1e..192e8c6a 100644 --- a/docs/design.txt +++ b/docs/design.txt @@ -56,7 +56,7 @@ Primary classes: interface language is instantiated as $wgLang, and the local content language as $wgContLang; be sure to use the *correct* language object depending upon the circumstances. - See also language.txt. + See also language.txt. Parser Class used to transform wikitext to html. diff --git a/docs/distributors.txt b/docs/distributors.txt new file mode 100644 index 00000000..5586df12 --- /dev/null +++ b/docs/distributors.txt @@ -0,0 +1,192 @@ +This document is intended to provide useful advice for parties seeking to +redistribute MediaWiki to end users. It's targeted particularly at maintainers +for Linux distributions, since it's been observed that distribution packages of +MediaWiki often break. We've consistently had to recommend that users seeking +support use official tarballs instead of their distribution's packages, and +this often solves whatever problem the user is having. It would be nice if +this could change. + +== Background: why web applications are different == + +MediaWiki is intended to be usable on any web host that provides support for +PHP and a database. Many users of low-end shared hosting have very limited +access to their machine: often only FTP access to some subdirectory of the web +root. Support for these users entails several restrictions, such as: + + 1) We cannot require installation of any files outside the web root. Few of + our users have access to directories like /usr or /etc. + 2) We cannot require the ability to run any utility on the command line. + Many shared hosts have exec() and similar PHP functions disabled. + 3) We cannot assume that the software has write access anywhere useful. The + user account that MediaWiki (including its installer) runs under is often + different from the account the user used to upload the files, and we might be + restricted by PHP settings such as safe mode or open_basedir. + 4) We cannot assume that the software even has read access anywhere useful. + Many shared hosts run all users' web applications under the same user, so + they can't rely on Unix permissions, and must forbid reads to even standard + directories like /tmp lest users read each others' files. + 5) We cannot assume that the user has the ability to install or run any + programs not written as web-accessible PHP scripts. + +Since anything that works on cheap shared hosting will work if you have shell +or root access too, MediaWiki's design is based around catering to the lowest +common denominator. Although we support higher-end setups as well (like +Wikipedia!), the way many things work by default is tailored toward shared +hosting. These defaults are unconventional from the point of view of normal +(non-web) applications -- they might conflict with distributors' policies, and +they certainly aren't ideal for someone who's installing MediaWiki as root. + +== Directory structure == + +Because of constraint (1) above, MediaWiki does not conform to normal +Unix filesystem layout. Hopefully we'll offer direct support for standard +layouts in the future, but for now *any change to the location of files is +unsupported*. Moving things and leaving symlinks will *probably* not break +anything, but it is *strongly* advised not to try any more intrusive changes to +get MediaWiki to conform more closely to your filesystem hierarchy. Any such +attempt will almost certainly result in unnecessary bugs. + +The standard recommended location to install MediaWiki, relative to the web +root, is /w (so, e.g., /var/www/w). Rewrite rules can then be used to enable +"pretty URLs" like /wiki/Article instead of /w/index.php?title=Article. (This +is the convention Wikipedia uses.) In theory, it should be possible to enable +the appropriate rewrite rules by default, if you can reconfigure the web +server, but you'd need to alter LocalSettings.php too. See + for details on short URLs. + +If you really must mess around with the directory structure, note that the +following files *must* all be web-accessible for MediaWiki to function +correctly: + + * api.php, img_auth.php, index.php, mwScriptLoader.php, opensearch_desc.php, + profileinfo.php, redirect.php, thumb.php, trackback.php. These are the entry + points for normal usage. This list may be incomplete and is subject to + change. + * config/index.php: Used for web-based installation (sets up the database, + prompts for the name of the wiki, etc.). No command-line installation is + currently available. + * images/: Used for uploaded files. This could be somewhere else if + $wgUploadDirectory and $wgUploadPath are changed appropriately. + * skins/*/: Subdirectories of skins/ contain CSS and JavaScript files that + must be accessible to web browsers. The PHP files and Skin.sample in skins/ + don't need to be accessible. This could be somewhere else if + $wgStyleDirectory and $wgStylePath are changed appropriately. + * extensions/: Many extensions include CSS and JavaScript files in their + extensions directory, and will break if they aren't web-accessible. Some + extensions might theoretically provide additional entry points as well, at + least in principle. + +But all files should keep their position relative to the web-visible +installation directory no matter what. If you must move includes/ somewhere in +/usr/share, provide a symlink from /var/www/w. If you don't, you *will* break +something. You have been warned. + +== Configuration == + +MediaWiki is configured using LocalSettings.php. This is a PHP file that's +generated when the user visits config/index.php to install the software, and +which the user can edit by hand thereafter. It's just a plain old PHP file, +and can contain any PHP statements. It usually sets global variables that are +used for configuration, and includes files used by any extensions. + +Distributors cannot easily add extra statements to the autogenerated +LocalSettings.php at the present time -- although hacking config/index.php +would work. It would be nice if this situation could be improved. + +Some configuration options that distributors might be in a position to set +intelligently: + + * $wgEmergencyContact: An e-mail address that can be used to contact the wiki + administrator. By default, "wikiadmin@$wgServerName". + * $wgPasswordSender: The e-mail address to use when sending password e-mails. + By default, "MediaWiki Mail ". + * $wgSMTP: Can be configured to use SMTP for mail sending instead of PHP + mail(). + +== Documentation == + +MediaWiki's official documentation is split between two places: the source +code, and . The source code documentation is written +exclusively by developers, and so is likely to be reliable (at worst, +outdated). However, it can be pretty sparse. mediawiki.org documentation is +often much more thorough, but it's maintained by a wiki that's open to +anonymous edits, so its quality is sometimes sketchy -- don't assume that +anything there is officially endorsed! + +== Upstream == + +MediaWiki is a project hosted and led by the Wikimedia Foundation, the +not-for-profit charity that operates Wikipedia. Wikimedia employs the lead +developer and several other paid developers, but commit access is given out +liberally and there are multiple very active volunteer developers as well. A +list of developers can be found at . + +MediaWiki's bug tracker is at . However, most +developers follow the bug tracker little or not at all. The best place to +post if you want to get developers' attention is the wikitech-l mailing list +. Posts to wikitech-l +will inevitably be read by multiple experienced MediaWiki developers. There's +also an active IRC chat at , where there are +usually several developers at reasonably busy times of day. + +Unfortunately, we don't have a very good system for patch review. Patches +should be submitted on Bugzilla (as unified diffs produced with "svn diff" +against the latest trunk revision), but many patches languish without review +until they bitrot into uselessness. You might want to get a developer to +commit to reviewing your patch before you put too much effort into it. +Reasonably straightforward patches shouldn't be too hard to get accepted if +there's an interested developer, however -- posting to Bugzilla and then +dropping a note on wikitech-l if nobody responds is a good tactic. + +All redistributors of MediaWiki should be subscribed to mediawiki-announce +. It's +extremely low-traffic, with an average of less than one post per month. All +new releases are announced here, including critical security updates. + +== Useful software to install == + +There are several other pieces of software that MediaWiki can make good use of. +Distributors might choose to install these automatically with MediaWiki and +perhaps configure it to use them (see Configuration section of this document): + + * APC (Alternative PHP Cache), XCache, or similar: Will greatly speed up the + execution of MediaWiki, and all other PHP applications, at some cost in + memory usage. Will be used automatically for the most part. + * clamav: Can be used for virus scanning of uploaded files. Enable with + "$wgAntivirus = 'clamav';". + * DjVuLibre: Allows processing of DjVu files. To enable this, set + "$wgDjvuDump = 'djvudump'; $wgDjvuRenderer = 'ddjvu'; $wgDjvuTxt = 'djvutxt';". + * HTML Tidy: Fixes errors in HTML at runtime. Can be enabled with "$wgUseTidy + = true;". + * ImageMagick: For resizing images. "$wgUseImageMagick = true;" will enable + it. PHP's GD can also be used, but ImageMagick is preferable. + * Squid: Can provide a drastic speedup and a major cut in resource + consumption, but enabling it may interfere with other applications. It might + be suitable for a separate mediawiki-squid package. For setup details, see: + + * rsvg or other SVG rasterizer: ImageMagick can be used for SVG support, but + is not ideal. Wikipedia (as of the time of this writing) uses rsvg. To + enable, set "$wgSVGConverter = 'rsvg';" (or other as appropriate). + * texvc: Included with MediaWiki. Instructions for compiling and + installing it are in the math/ directory. + +MediaWiki uses some standard GNU utilities as well, such as diff and diff3. If +these are present in /usr/bin or some other reasonable location, they will be +used automatically. + +MediaWiki also has a "job queue" that handles background processing. Because +shared hosts often don't provide access to cron, the job queue is run on every +page view by default. This means the background tasks aren't really done in +the background. Busy wikis can set $wgJobRunRate to 0 and run +maintenance/runJobs.php periodically out of cron. Distributors probably +shouldn't set this up as a default, however, since the extra cron job is +unnecessary overhead for a little-used wiki. + +== Web server configuration == + +MediaWiki includes several .htaccess files to restrict access to some +directories. If the web server is not configured to support these files, and +the relevant directories haven't been moved someplace inaccessible anyway (e.g. +symlinked in /usr/share with the web server configured to not follow symlinks), +then it might be useful to deny web access to those directories in the web +server's configuration. diff --git a/docs/export-0.4.xsd b/docs/export-0.4.xsd new file mode 100644 index 00000000..9ff39254 --- /dev/null +++ b/docs/export-0.4.xsd @@ -0,0 +1,212 @@ + + + + + + + MediaWiki's page export format + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/export-demo.xml b/docs/export-demo.xml index 1b4bd7cf..77b26a41 100644 --- a/docs/export-demo.xml +++ b/docs/export-demo.xml @@ -1,4 +1,4 @@ - + @@ -49,6 +49,10 @@ 1 + + + + edit=sysop:move=sysop @@ -112,4 +116,19 @@ + + 15 + 2008-10-23T03:20:32Z + + Wikimedian + 12345 + + content was: 'I think this was a silly edit' + delete + delete + Silly page name + + + + diff --git a/docs/hooks.txt b/docs/hooks.txt index f973d6b8..174fb7d9 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -16,10 +16,10 @@ event hook A clump of code and data that should be run when an event happens. This can be either a function and a chunk of data, or an object and a method. - + hook function The function part of a hook. - + ==Rationale== Hooks allow us to decouple optionally-run code from code that is run for @@ -54,21 +54,21 @@ email notification when an article is shown may add: function showAnArticle($article) { global $wgReverseTitle, $wgCapitalizeTitle, $wgNotifyArticle; - + if ($wgReverseTitle) { wfReverseTitle($article); } - + if ($wgCapitalizeTitle) { wfCapitalizeTitle($article); } # code to actually show the article goes here - + if ($wgNotifyArticle) { wfNotifyArticleShow($article)); } - } + } Using a hook-running strategy, we can avoid having all this option-specific stuff in our mainline code. Using hooks, the function becomes: @@ -87,7 +87,7 @@ We've cleaned up the code here by removing clumps of weird, infrequently used code and moving them off somewhere else. It's much easier for someone working with this code to see what's _really_ going on, and make changes or fix bugs. -In addition, we can take all the code that deals with the little-used +In addition, we can take all the code that deals with the little-used title-reversing options (say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle, deleteAnArticle, exportArticle, etc., we can concentrate it all in an extension @@ -116,8 +116,8 @@ Having all this code related to the title-reversion option in one place means that it's easier to read and understand; you don't have to do a grep-find to see where the $wgReverseTitle variable is used, say. -If the code is well enough isolated, it can even be excluded when not used -- -making for some slight savings in memory and load-up performance at runtime. +If the code is well enough isolated, it can even be excluded when not used -- +making for some slight savings in memory and load-up performance at runtime. Admins who want to have all the reversed titles can add: require_once('extensions/ReverseTitle.php'); @@ -162,7 +162,7 @@ would result in the following code being executed when 'EventName' happened: $object->someMethod($param1, $param2) # object with method and data $object->someMethod($someData, $param1, $param2) - + Note that when an object is the hook, and there's no specified method, the default method called is 'onEventName'. For different events this would be different: 'onArticleSave', 'onUserLogin', etc. @@ -183,13 +183,13 @@ Hooks can return three possible values: should be shown to the user * false: the hook has successfully done the work necessary and the calling function should skip - + The last result would be for cases where the hook function replaces the main functionality. For example, if you wanted to authenticate users to a custom system (LDAP, another PHP program, whatever), you could do: $wgHooks['UserLogin'][] = array('ldapLogin', $ldapServer); - + function ldapLogin($username, $password) { # log user into LDAP return false; @@ -199,7 +199,7 @@ Returning false makes less sense for events where the action is complete, and will normally be ignored. Note that none of the examples made use of create_function() as a way to -attach a function to a hook. This is known to cause problems (notably with +attach a function to a hook. This is known to cause problems (notably with Special:Version), and should be avoided when at all possible. ==Using hooks== @@ -207,7 +207,7 @@ Special:Version), and should be avoided when at all possible. A calling function or method uses the wfRunHooks() function to run the hooks related to a particular event, like so: - class Article { + class Article { # ... function protect() { global $wgUser; @@ -217,7 +217,7 @@ related to a particular event, like so: } } } - + wfRunHooks() returns true if the calling function should continue processing (the hooks ran OK, or there are no hooks to run), or false if it shouldn't (an error occurred, or one of the hooks handled the action already). Checking the @@ -270,7 +270,7 @@ is enabled ( $wgUseAjax = true; ). 'AlternateEdit': before checking if an user can edit a page and before showing the edit form ( EditPage::edit() ). This is triggered on &action=edit. -$EditPage : the EditPage object +$EditPage: the EditPage object 'APIAfterExecute': after calling the execute() method of an API module. Use this to extend core API modules. @@ -282,7 +282,7 @@ fail, returning an error message or an tag if $resultArr was filled. $EditPage : the EditPage object $text : the new text of the article (has yet to be saved) -$resultArr : data in this array will be added to the API result +&$resultArr : data in this array will be added to the API result 'APIGetAllowedParams': use this hook to modify a module's parameters. &$module: Module object @@ -324,7 +324,8 @@ associated Revision object. In the hook, just add your callback to the $tokenFunctions array and return true (returning false makes no sense) $tokenFunctions: array(action => callback) -'APIQueryRecentChangesTokens': use this hook to add custom tokens to list=recentchanges. +'APIQueryRecentChangesTokens': use this hook to add custom tokens to +list=recentchanges. Every token has an action, which will be used in the rctoken parameter and in the output (actiontoken="..."), and a callback function which should return the token, or false if the user isn't allowed to obtain @@ -335,10 +336,26 @@ associated RecentChange object. In the hook, just add your callback to the $tokenFunctions array and return true (returning false makes no sense) $tokenFunctions: array(action => callback) -'ArticleAfterFetchContent': after fetching content of an article from the database +'APIQueryUsersTokens': use this hook to add custom token to list=users. +Every token has an action, which will be used in the ustoken parameter +and in the output (actiontoken="..."), and a callback function which +should return the token, or false if the user isn't allowed to obtain +it. The prototype of the callback function is func($user) where $user +is the User object. In the hook, just add your callback to the +$tokenFunctions array and return true (returning false makes no sense) +$tokenFunctions: array(action => callback) + +'ArticleAfterFetchContent': after fetching content of an article from +the database $article: the article (object) being loaded from the database $content: the content (string) of the article +'ArticleConfirmDelete': before writing the confirmation form for article + deletion +$article: the article (object) being deleted +$output: the OutputPage object ($wgOut) +&$reason: the reason (string) the article is being deleted + 'ArticleDelete': before an article is deleted $article: the article (object) being deleted $user: the user (object) deleting the article @@ -352,18 +369,23 @@ $user: the user that deleted the article $reason: the reason the article was deleted $id: id of the article that was deleted -'ArticleEditUpdateNewTalk': before updating user_newtalk when a user talk page was changed +'ArticleEditUpdateNewTalk': before updating user_newtalk when a user talk page +was changed $article: article (object) of the user talk page -'ArticleEditUpdates': when edit updates (mainly link tracking) are made when an article has been changed +'ArticleEditUpdates': when edit updates (mainly link tracking) are made when an +article has been changed $article: the article (object) -$editInfo: data holder that includes the parser output ($editInfo->output) for that page after the change +$editInfo: data holder that includes the parser output ($editInfo->output) for +that page after the change $changed: bool for if the page was changed -'ArticleEditUpdatesDeleteFromRecentchanges': before deleting old entries from recentchanges table, return false to not delete old entries +'ArticleEditUpdatesDeleteFromRecentchanges': before deleting old entries from +recentchanges table, return false to not delete old entries $article: article (object) being modified -'ArticleFromTitle': when creating an article object from a title object using Wiki::articleFromTitle() +'ArticleFromTitle': when creating an article object from a title object using +Wiki::articleFromTitle() $title: title (object) used to create the article object $article: article (object) that will be returned @@ -380,7 +402,7 @@ $revision: New Revision of the article 'ArticleMergeComplete': after merging to article using Special:Mergehistory $targetTitle: target title (object) -$destTitle: destination title (object) +$destTitle: destination title (object) 'ArticlePageDataAfter': after loading data of an article from the database $article: article (object) whose data were loaded @@ -404,7 +426,7 @@ $protect: boolean whether it was a protect or an unprotect $reason: Reason for protect $moveonly: boolean whether it was for move only or not -'ArticlePurge': before executing "&action=purge" +'ArticlePurge': before executing "&action=purge" $article: article (object) to purge 'ArticleRevisionVisiblitySet': called when changing visibility of one or more @@ -447,18 +469,22 @@ $baseRevId: the rev ID (or false) this edit was based on $title: Title corresponding to the article restored $create: Whether or not the restoration caused the page to be created (i.e. it didn't exist before) +$comment: The comment associated with the undeletion. -'ArticleUpdateBeforeRedirect': After a page is updated (usually on save), before the user is redirected back to the page +'ArticleUpdateBeforeRedirect': After a page is updated (usually on save), +before the user is redirected back to the page &$article: the article &$sectionanchor: The section anchor link (e.g. "#overview" ) &$extraq: Extra query parameters which can be added via hooked functions -'ArticleViewHeader': Before the parser cache is about to be tried for article viewing. +'ArticleViewHeader': Before the parser cache is about to be tried for article +viewing. &$article: the article &$pcache: whether to try the parser cache or not &$outputDone: whether the output for this page finished or not -'ArticleViewRedirect': before setting "Redirected from ..." subtitle when follwed an redirect +'ArticleViewRedirect': before setting "Redirected from ..." subtitle when +follwed an redirect $article: target article (object) 'AuthPluginAutoCreate': Called when creating a local account for an user logged @@ -484,9 +510,17 @@ rendered inline in wiki pages or galleries in category pages. 'BeforeGalleryFindFile': before an image is fetched for a gallery &$gallery,: the gallery object -&$nt: the image title +&$nt: the image title &$time: image timestamp +'BeforeInitialize': before anything is initialized in performRequestForTitle() +&$title: Title being used for request +&$article: The associated Article object +&$output: OutputPage object +&$user: User +$request: WebRequest object +$mediaWiki: Mediawiki object + 'BeforePageDisplay': Prior to outputting a page &$out: OutputPage object &$skin: Skin object @@ -518,18 +552,21 @@ $user: the user who did the block (not the one being blocked) 'BookInformation': Before information output on Special:Booksources $isbn: ISBN to show information for $output: OutputPage object in use - + 'CategoryPageView': before viewing a categorypage in CategoryPage::view $catpage: CategoryPage instance 'ChangesListInsertArticleLink': Override or augment link to article in RC list. -&$this: ChangesList instance. +&$changesList: ChangesList instance. &$articlelink: HTML of link to article (already filled-in). &$s: HTML of row that is being constructed. &$rc: RecentChange instance. $unpatrolled: Whether or not we are showing unpatrolled changes. $watched: Whether or not the change is watched by the user. +'ConfirmEmailComplete': Called after a user's email has been confirmed successfully +$user: user (object) whose email is being confirmed + 'ContribsPager::getQueryInfo': Before the contributions query is about to run &$pager: Pager object for contributions &queryInfo: The query for the contribs Pager @@ -552,6 +589,9 @@ Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. +'DatabaseOraclePostInit': Called after initialising an Oracle database +&$db: the DatabaseOracle object + 'NewDifferenceEngine': Called when a new DifferenceEngine object is made $title: the diff page title (nullable) &$oldId: the actual old Id to use in the diff @@ -564,7 +604,8 @@ $diff: DifferenceEngine object that's calling $oldRev: Revision object of the "old" revision (may be null/invalid) $newRev: Revision object of the "new" revision -'DisplayOldSubtitle': before creating subtitle when browsing old versions of an article +'DisplayOldSubtitle': before creating subtitle when browsing old versions of +an article $article: article (object) being viewed $oldid: oldid (int) being viewed @@ -589,16 +630,27 @@ $summary: Edit summary for page 'EditFilterMerged': Post-section-merge edit filter $editor: EditPage instance (object) $text: content of the edit box -$error: error message to return +&$error: error message to return $summary: Edit summary for page -'EditFormPreloadText': Allows population of the edit form when creating new pages +'EditFormPreloadText': Allows population of the edit form when creating +new pages &$text: Text to preload with &$title: Title object representing the page being created +'EditFormInitialText': Allows modifying the edit form when editing existing +pages +$editPage: EditPage object + 'EditPage::attemptSave': called before an article is saved, that is before insertNewArticle() is called -&$editpage_Obj: the current EditPage object +$editpage_Obj: the current EditPage object + +'EditPage::importFormData': allow extensions to read additional data +posted in the form +$editpage: EditPage instance +$request: Webrequest +return value is ignored (should always return true) 'EditPage::showEditForm:fields': allows injection of form field into edit form &$editor: the EditPage instance for reference @@ -608,10 +660,10 @@ return value is ignored (should always return true) 'EditPage::showEditForm:initial': before showing the edit form $editor: EditPage instance (object) -Return false to halt editing; you'll need to handle error messages, etc. yourself. -Alternatively, modifying $error and returning true will cause the contents of $error -to be echoed at the top of the edit form as wikitext. Return true without altering -$error to allow the edit to proceed. +Return false to halt editing; you'll need to handle error messages, etc. +yourself. Alternatively, modifying $error and returning true will cause the +contents of $error to be echoed at the top of the edit form as wikitext. +Return true without altering $error to allow the edit to proceed. 'EditPageBeforeConflictDiff': allows modifying the EditPage object and output when there's an edit conflict. Return false to halt normal diff output; in @@ -621,16 +673,45 @@ sections. &$editor: EditPage instance &$out: OutputPage instance -'EditPageBeforeEditButtons': allows modifying the edit buttons below the textarea in the edit form +'EditPageBeforeEditButtons': allows modifying the edit buttons below the +textarea in the edit form &$editpage: The current EditPage object &$buttons: Array of edit buttons "Save", "Preview", "Live", and "Diff" &$tabindex: HTML tabindex of the last edit check/button -'EditPageBeforeEditChecks': allows modifying the edit checks below the textarea in the edit form +'EditPageBeforeEditChecks': allows modifying the edit checks below the +textarea in the edit form &$editpage: The current EditPage object &$checks: Array of edit checks like "watch this page"/"minor edit" &$tabindex: HTML tabindex of the last edit check/button +'EditPageBeforeEditToolbar': allows modifying the edit toolbar above the +textarea in the edit form +&$toolbar: The toolbar HTMl + +'EditPageCopyrightWarning': Allow for site and per-namespace customization of contribution/copyright notice. +$title: title of page being edited +&$msg: localization message name, overridable. Default is either 'copyrightwarning' or 'copyrightwarning2' + +'EditPageGetDiffText': Allow modifying the wikitext that will be used in +"Show changes" +$editPage: EditPage object +&$newtext: wikitext that will be used as "your version" + +'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed +$editPage: EditPage object +&$toparse: wikitext that will be parsed + +'EditPageNoSuchSection': When a section edit request is given for an non-existent section +&$editpage: The current EditPage object +&$res: the HTML of the error text + +'EditPageTosSummary': Give a chance for site and per-namespace customizations +of terms of service summary link that might exist separately from the copyright +notice. +$title: title of page being edited +&$msg: localization message name, overridable. Default is 'editpage-tos-summary' + 'EditSectionLink': Do not use, use DoEditSectionLink instead. $skin: Skin rendering the UI $title: Title being linked to @@ -656,17 +737,23 @@ $from: address of sending user $subject: subject of the mail $text: text of the mail -'FetchChangesList': When fetching the ChangesList derivative for a particular user +'EmailUserPermissionsErrors': to retrieve permissions errors for emailing a user. +$user: The user who is trying to email another user. +$editToken: The user's edit token. +&$hookErr: Out-param for the error. Passed as the parameters to OutputPage::showErrorPage. + +'FetchChangesList': When fetching the ChangesList derivative for +a particular user &$user: User the list is being fetched for &$skin: Skin object to be used with the list -&$list: List object (defaults to NULL, change it to an object instance and return -false override the list derivative used) +&$list: List object (defaults to NULL, change it to an object + instance and return false override the list derivative used) 'FileDeleteComplete': When a file is deleted $file: reference to the deleted file $oldimage: in case of the deletion of an old image, the name of the old file -$article: in case all revisions of the file are deleted a reference to the article - associated with the file. +$article: in case all revisions of the file are deleted a reference to the + article associated with the file. $user: user who performed the deletion $reason: reason @@ -679,7 +766,8 @@ $fileVersions: array of undeleted versions. Empty if all versions were restored $user: user who performed the undeletion $reason: reason -'GetAutoPromoteGroups': When determining which autopromote groups a user is entitled to be in. +'GetAutoPromoteGroups': When determining which autopromote groups a user +is entitled to be in. &$user: user to promote. &$promote: groups that will be added. @@ -702,7 +790,8 @@ $url: string value as output (out parameter, can modify) $query: query options passed to Title::getInternalURL() 'GetLinkColours': modify the CSS class of an array of page links -$linkcolour_ids: array of prefixed DB keys of the pages linked to, indexed by page_id. +$linkcolour_ids: array of prefixed DB keys of the pages linked to, + indexed by page_id. &$colours: (output) array of CSS classes, indexed by prefixed DB keys 'GetLocalURL': modify local URLs as output into page links @@ -710,6 +799,10 @@ $title: Title object of page $url: string value as output (out parameter, can modify) $query: query options passed to Title::getLocalURL() +'GetPreferences': modify user preferences +$user: User whose preferences are being modified. +&$preferences: Preferences description array, to be fed to an HTMLForm object + 'getUserPermissionsErrors': Add a permissions error when permissions errors are checked for. Use instead of userCan for most cases. Return false if the user can't do it, and populate $result with the reason in the form of @@ -725,13 +818,14 @@ $result: User permissions error to add. If none, return true. 'getUserPermissionsErrorsExpensive': Absolutely the same, but is called only if expensive checks are enabled. -'HTMLCacheUpdate::doUpdate': After cache invalidation updates are inserted into the job queue. +'HTMLCacheUpdate::doUpdate': After cache invalidation updates are inserted +into the job queue. $title: Title object, pages linked to this title are purged. 'ImageBeforeProduceHTML': Called before producing the HTML created by a wiki image insertion. You can skip the default logic entirely by returning false, or just modify a few things using call-by-reference. -&$this: Skin object +&$skin: Skin object &$title: Title object of the image &$file: File object, or false if it doesn't exist &$frameParams: Various parameters with special meanings; see documentation in @@ -747,6 +841,11 @@ $title: Title object, pages linked to this title are purged. $imagePage: ImagePage object ($this) $output: $wgOut +'ImagePageAfterImageLinks': called after the image links section on an image + page is built +$imagePage: ImagePage object ($this) +&$html: HTML for the hook to add + 'ImagePageFileHistoryLine': called when a file history line is contructed $file: the file $line: the HTML of the history line @@ -757,6 +856,24 @@ $page: ImagePage object &$file: File object &$displayFile: displayed File object +'ImagePageShowTOC': called when the file toc on an image page is generated +$page: ImagePage object +&$toc: Array of
  • strings + +'ImgAuthBeforeStream': executed before file is streamed to user, but only when + using img_auth.php +&$title: the Title object of the file as it would appear for the upload page +&$path: the original file and path name when img_auth was invoked by the the web + server +&$name: the name only component of the file +&$result: The location to pass back results of the hook routine (only used if + failed) + $result[0]=The index of the header message + $result[1]=The index of the body text message + $result[2 through n]=Parameters passed to body text message. Please note the + header message cannot receive/use parameters. + + 'InitializeArticleMaybeRedirect': MediaWiki check to see if title is a redirect $title: Title object ($wgTitle) $request: WebRequest @@ -764,15 +881,14 @@ $ignoreRedirect: boolean to skip redirect check $target: Title/string of redirect target $article: Article object -'InitPreferencesForm': called at the end of PreferencesForm's constructor -$form: the PreferencesForm -$request: the web request to initialized from - -'InternalParseBeforeLinks': during Parser's internalParse method before links but -after noinclude/includeonly/onlyinclude and other processing. -&$this: Parser object +'InternalParseBeforeLinks': during Parser's internalParse method before links +but after noinclude/includeonly/onlyinclude and other processing. +&$parser: Parser object &$text: string containing partially parsed text -&$this->mStripState: Parser's internal StripState object +&$stripState: Parser's internal StripState object + +'InvalidateEmailComplete': Called after a user's email has been invalidated successfully +$user: user (object) whose email is being invalidated 'IsFileCacheable': Override the result of Article::isFileCacheable() (if true) $article: article (object) being checked @@ -791,11 +907,15 @@ $password: The password entered by the user &$result: Set this and return false to override the internal checks $user: User the password is being validated for -'LanguageGetMagic': Use this to define synonyms of magic words depending of the language +'LanguageGetMagic': DEPRECATED, use $magicWords in a file listed in +$wgExtensionMessagesFiles instead. +Use this to define synonyms of magic words depending of the language $magicExtensions: associative array of magic words synonyms $lang: laguage code (string) -'LanguageGetSpecialPageAliases': Use to define aliases of special pages names depending of the language +'LanguageGetSpecialPageAliases': DEPRECATED, use $specialPageAliases in a file +listed in $wgExtensionMessagesFiles instead. +Use to define aliases of special pages names depending of the language $specialPageAliases: associative array of magic words synonyms $lang: laguage code (string) @@ -828,22 +948,26 @@ $options: the options. Will always include either 'known' or 'broken', and may ciative array form. &$ret: the value to return if your hook returns false. -'LinkerMakeExternalImage': At the end of Linker::makeExternalImage() just before the return +'LinkerMakeExternalImage': At the end of Linker::makeExternalImage() just +before the return &$url: the image url &$alt: the image's alt text &$img: the new image HTML (if returning false) -'LinkerMakeExternalLink': At the end of Linker::makeExternalLink() just before the return +'LinkerMakeExternalLink': At the end of Linker::makeExternalLink() just +before the return &$url: the link url &$text: the link text &$link: the new link HTML (if returning false) &$attribs: the attributes to be applied. $linkType: The external link type -'LinksUpdate': At the beginning of LinksUpdate::doUpdate() just before the actual update +'LinksUpdate': At the beginning of LinksUpdate::doUpdate() just before the +actual update &$linksUpdate: the LinkUpdate object -'LinksUpdateComplete': At the end of LinksUpdate::doUpdate() when updating has completed +'LinksUpdateComplete': At the end of LinksUpdate::doUpdate() when updating has +completed &$linksUpdate: the LinkUpdate object 'LinksUpdateConstructed': At the end of LinksUpdate() is contruction. @@ -852,9 +976,8 @@ $linkType: The external link type 'ListDefinedTags': When trying to find all defined tags. &$tags: The list of tags. -'LoadAllMessages': called by MessageCache::loadAllMessages() to load extensions messages - -'LoadExtensionSchemaUpdates': called by maintenance/updaters.inc when upgrading database schema +'LoadExtensionSchemaUpdates': called by maintenance/updaters.inc when upgrading +database schema 'LocalFile::getHistory': called before file history query performed $file: the file @@ -864,96 +987,129 @@ $conds: conditions $opts: query options $join_conds: JOIN conditions -'LoginAuthenticateAudit': a login attempt for a valid user account either succeeded or failed. - No return data is accepted; this hook is for auditing only. +'LocalisationCacheRecache': Called when loading the localisation data into cache +$cache: The LocalisationCache object +$code: language code +&$alldata: The localisation data from core and extensions + +'LoginAuthenticateAudit': a login attempt for a valid user account either +succeeded or failed. No return data is accepted; this hook is for auditing only. $user: the User object being authenticated against $password: the password being submitted and found wanting -$retval: a LoginForm class constant with authenticateUserData() return value (SUCCESS, WRONG_PASS, etc) +$retval: a LoginForm class constant with authenticateUserData() return + value (SUCCESS, WRONG_PASS, etc) 'LogLine': Processes a single log entry on Special:Log -$log_type: string for the type of log entry (e.g. 'move'). Corresponds to logging.log_type - database field. -$log_action: string for the type of log action (e.g. 'delete', 'block', 'create2'). Corresponds - to logging.log_action database field. -$title: Title object that corresponds to logging.log_namespace and logging.log_title database fields. -$paramArray: Array of parameters that corresponds to logging.log_params field. Note that only $paramArray[0] - appears to contain anything. -&$comment: string that corresponds to logging.log_comment database field, and which is displayed in the UI. +$log_type: string for the type of log entry (e.g. 'move'). Corresponds to + logging.log_type database field. +$log_action: string for the type of log action (e.g. 'delete', 'block', + 'create2'). Corresponds to logging.log_action database field. +$title: Title object that corresponds to logging.log_namespace and + logging.log_title database fields. +$paramArray: Array of parameters that corresponds to logging.log_params field. + Note that only $paramArray[0] appears to contain anything. +&$comment: string that corresponds to logging.log_comment database field, and + which is displayed in the UI. &$revert: string that is displayed in the UI, similar to $comment. -$time: timestamp of the log entry (added in 1.12) +$time: timestamp of the log entry (added in 1.12) -'LogPageValidTypes': action being logged. DEPRECATED: Use $wgLogTypes +'LogPageValidTypes': action being logged. +DEPRECATED: Use $wgLogTypes &$type: array of strings -'LogPageLogName': name of the logging page(s). DEPRECATED: Use $wgLogNames +'LogPageLogName': name of the logging page(s). +DEPRECATED: Use $wgLogNames &$typeText: array of strings -'LogPageLogHeader': strings used by wfMsg as a header. DEPRECATED: Use $wgLogHeaders +'LogPageLogHeader': strings used by wfMsg as a header. +DEPRECATED: Use $wgLogHeaders &$headerText: array of strings -'LogPageActionText': strings used by wfMsg as a header. DEPRECATED: Use $wgLogActions +'LogPageActionText': strings used by wfMsg as a header. +DEPRECATED: Use $wgLogActions &$actionText: array of strings -'MagicWordMagicWords': When defining new magic word. DEPRECATED: Use LanguageGetMagic hook instead +'MagicWordMagicWords': When defining new magic word. +DEPRECATED: use $magicWords in a file listed in +$wgExtensionMessagesFiles instead. $magicWords: array of strings 'MagicWordwgVariableIDs': When definig new magic words IDs. $variableIDs: array of strings -'MakeGlobalVariablesScript': called right before Skin::makeVariablesScript is executed -&$vars: variable (or multiple variables) to be added into the output - of Skin::makeVariablesScript +'MakeGlobalVariablesScript': called right before Skin::makeVariablesScript +is executed +&$vars: variable (or multiple variables) to be added into the output + of Skin::makeVariablesScript 'MarkPatrolled': before an edit is marked patrolled $rcid: ID of the revision to be marked patrolled $user: the user (object) marking the revision as patrolled $wcOnlySysopsCanPatrol: config setting indicating whether the user - needs to be a sysop in order to mark an edit patrolled + needs to be a sysop in order to mark an edit patrolled 'MarkPatrolledComplete': after an edit is marked patrolled $rcid: ID of the revision marked as patrolled $user: user (object) who marked the edit patrolled $wcOnlySysopsCanPatrol: config setting indicating whether the user - must be a sysop to patrol the edit + must be a sysop to patrol the edit 'MathAfterTexvc': after texvc is executed when rendering mathematics $mathRenderer: instance of MathRenderer $errmsg: error message, in HTML (string). Nonempty indicates failure - of rendering the formula + of rendering the formula 'MediaWikiPerformAction': Override MediaWiki::performAction(). Use this to do something completely different, after the basic globals have been set up, but before ordinary actions take place. -$output: $wgOut +$output: $wgOut $article: $wgArticle -$title: $wgTitle -$user: $wgUser +$title: $wgTitle +$user: $wgUser $request: $wgRequest -$this: The $mediawiki object +$mediaWiki: The $mediawiki object 'MessagesPreLoad': When loading a message from the database $title: title of the message (string) $message: value (string), change it to the message you want to define -'MonoBookTemplateToolboxEnd': Called by Monobook skin after toolbox links have been rendered (useful for adding more) -Note: this is only run for the Monobook skin. To add items to the toolbox -for all 'SkinTemplate'-type skins, use the SkinTemplateToolboxEnd hook -instead. +'MessageCacheReplace': When a message page is changed. +Useful for updating caches. +$title: name of the page changed. +$text: new contents of the page. + +'ModifyExportQuery': Modify the query used by the exporter. +$db: The database object to be queried. +&$tables: Tables in the query. +&$conds: Conditions in the query. +&$opts: Options for the query. +&$join_conds: Join conditions for the query. + +'MonoBookTemplateToolboxEnd': Called by Monobook skin after toolbox links have +been rendered (useful for adding more) +Note: this is only run for the Monobook skin. This hook is deprecated and +may be removed in the future. To add items to the toolbox you should use +the SkinTemplateToolboxEnd hook instead, which works for all +'SkinTemplate'-type skins. $tools: array of tools -'NewRevisionFromEditComplete': called when a revision was inserted due to an edit +'NewRevisionFromEditComplete': called when a revision was inserted +due to an edit $article: the article edited $rev: the new revision $baseID: the revision ID this was based off, if any $user: the editing user 'NormalizeMessageKey': Called before the software gets the text of a message - (stuff in the MediaWiki: namespace), useful for changing WHAT message gets displayed -&$key: the message being looked up. Change this to something else to change what message gets displayed (string) +(stuff in the MediaWiki: namespace), useful for changing WHAT message gets +displayed +&$key: the message being looked up. Change this to something else to change + what message gets displayed (string) &$useDB: whether or not to look up the message in the database (bool) &$langCode: the language code to get the message for (string) - or - - whether to use the content language (true) or site language (false) (bool) -&$transform: whether or not to expand variables and templates in the message (bool) + whether to use the content language (true) or site language (false) (bool) +&$transform: whether or not to expand variables and templates + in the message (bool) 'OldChangesListRecentChangesLine': Customize entire Recent Changes line. &$changeslist: The OldChangesList instance. @@ -964,23 +1120,33 @@ $user: the editing user Hooks can alter or append to the array of URLs for search & suggestion formats. &$urls: array of associative arrays with Url element attributes +'OtherBlockLogLink': Get links to the block log from extensions which blocks + users and/or IP addresses too +$otherBlockLink: An array with links to other block logs +$ip: The requested IP address or username + 'OutputPageBeforeHTML': a page has been processed by the parser and -the resulting HTML is about to be displayed. -$parserOutput: the parserOutput (object) that corresponds to the page +the resulting HTML is about to be displayed. +$parserOutput: the parserOutput (object) that corresponds to the page $text: the text that will be displayed, in HTML (string) -'OutputPageCheckLastModified': when checking if the page has been modified since the last visit -&$modifiedTimes: array of timestamps, the following keys are set: page, user, epoch +'OutputPageCheckLastModified': when checking if the page has been modified +since the last visit +&$modifiedTimes: array of timestamps. + The following keys are set: page, user, epoch 'OutputPageParserOutput': after adding a parserOutput to $wgOut $out: OutputPage instance (object) $parserOutput: parserOutput instance being added in $out -'OutputPageMakeCategoryLinks': links are about to be generated for the page's categories. - Implementations should return false if they generate the category links, so the default link generation is skipped. +'OutputPageMakeCategoryLinks': links are about to be generated for the page's +categories. Implementations should return false if they generate the category +links, so the default link generation is skipped. $out: OutputPage instance (object) -$categories: associative array, keys are category names, values are category types ("normal" or "hidden") -$links: array, intended to hold the result. Must be an associative array with category types as keys and arrays of HTML links as values. +$categories: associative array, keys are category names, values are category + types ("normal" or "hidden") +$links: array, intended to hold the result. Must be an associative array with + category types as keys and arrays of HTML links as values. 'PageHistoryBeforeList': When a history page list is about to be constructed. $article: the article that the history is loading for @@ -988,20 +1154,22 @@ $article: the article that the history is loading for 'PageHistoryLineEnding' : right before the end
  • is added to a history line $row: the revision row for this line $s: the string representing this parsed line +$classes: array containing the
  • element classes -'PageHistoryPager::getQueryInfo': when a history pager query parameter set is constructed +'PageHistoryPager::getQueryInfo': when a history pager query parameter set +is constructed $pager: the pager $queryInfo: the query parameters 'PageRenderingHash': alter the parser cache option hash key - A parser extension which depends on user options should install - this hook and append its values to the key. +A parser extension which depends on user options should install +this hook and append its values to the key. $hash: reference to a hash key string which can be modified 'ParserAfterStrip': Same as ParserBeforeStrip 'ParserAfterTidy': Called after Parser::tidy() in Parser::parse() -$parser: Parser object being used +$parser: Parser object being used $text: text that'll be returned 'ParserBeforeInternalParse': called at the beginning of Parser::internalParse() @@ -1009,13 +1177,14 @@ $parser: Parser object $text: text to parse $stripState: StripState instance being used -'ParserBeforeStrip': Called at start of parsing time (no more strip, deprecated ?) +'ParserBeforeStrip': Called at start of parsing time +(no more strip, deprecated ?) $parser: parser object $text: text being parsed $stripState: stripState used (object) 'ParserBeforeTidy': called before tidy and custom tags replacements -$parser: Parser object being used +$parser: Parser object being used $text: actual text 'ParserClearState': called at the end of Parser::clearState() @@ -1024,30 +1193,38 @@ $parser: Parser object being cleared 'ParserFirstCallInit': called when the parser initialises for the first time &$parser: Parser object being cleared -'ParserGetVariableValueSwitch': called when the parser need the value of a custom magic word +'ParserGetVariableValueSwitch': called when the parser need the value of a +custom magic word $parser: Parser object -$varCache: array to store the value in case of multiples calls of the same magic word +$varCache: array to store the value in case of multiples calls of the + same magic word $index: index (string) of the magic $ret: value of the magic word (the hook should set it) +$frame: PPFrame object to use for expanding any template variables -'ParserGetVariableValueTs': use this to change the value of the time for the {{LOCAL...}} magic word +'ParserGetVariableValueTs': use this to change the value of the time for the +{{LOCAL...}} magic word $parser: Parser object $time: actual time (timestamp) -'ParserGetVariableValueVarCache': use this to change the value of the variable cache or return false to not use it +'ParserGetVariableValueVarCache': use this to change the value of the +variable cache or return false to not use it $parser: Parser object $varCache: varaiable cache (array) -'ParserLimitReport': called at the end of Parser:parse() when the parser will include comments about size of the text parsed +'ParserLimitReport': called at the end of Parser:parse() when the parser will +include comments about size of the text parsed $parser: Parser object $limitReport: text that will be included (without comment tags) -'ParserMakeImageParams': Called before the parser make an image link, use this to modify the parameters of the image. +'ParserMakeImageParams': Called before the parser make an image link, use this +to modify the parameters of the image. $title: title object representing the file $file: file object that will be used to create the image &$params: 2-D array of parameters -'ParserTestParser': called when creating a new instance of Parser in maintenance/parserTests.inc +'ParserTestParser': called when creating a new instance of Parser in +maintenance/parserTests.inc $parser: Parser object created 'ParserTestTables': alter the list of tables to duplicate when parser tests @@ -1068,10 +1245,6 @@ $action : Action being performed Change $result and return false to give a definitive answer, otherwise the built-in rate limiting checks are used, if enabled. -'PreferencesUserInformationPanel': Add HTML bits to user information list in preferences form -$form : PreferencesForm object -&$html : HTML to append to - 'PrefixSearchBackend': Override the title prefix search used for OpenSearch and AJAX search suggestions. Put results into &$results outparam and return false. $ns : array of int namespace keys to search in @@ -1089,6 +1262,18 @@ $user: User (object) changing his passoword $newPass: new password $error: error (string) 'badretype', 'wrongpassword', 'error' or 'success' +'ProtectionForm::buildForm': called after all protection type fieldsets are made in the form +$article: the title being (un)protected +$output: a string of the form HTML so far + +'ProtectionForm::save': called when a protection form is submitted +$article: the title being (un)protected +$errorMsg: an html message string of an error + +'ProtectionForm::showLogExtract': called after the protection log extract is shown +$article: the page the form is shown for +$out: OutputPage object + 'RawPageViewBeforeOutput': Right before the text is blown out in action=raw &$obj: RawPage object &$text: The text that's going to be the output @@ -1096,14 +1281,6 @@ $error: error (string) 'badretype', 'wrongpassword', 'error' or 'success' 'RecentChange_save': called at the end of RecenChange::save() $recentChange: RecentChange object -'RenderPreferencesForm': called at the end of PreferencesForm::mainPrefsForm -$form: the PreferencesForm -$out: output page to render to, probably $wgOut - -'ResetPreferences': called at the end of PreferencesForm::resetPrefs -$form: the PreferencesForm -$user: the User object to load preferences from - 'RevisionInsertComplete': called after a revision is inserted into the DB &$revision: the Revision $data: the data stored in old_text. The meaning depends on $flags: if external @@ -1113,30 +1290,50 @@ $data: the data stored in old_text. The meaning depends on $flags: if external $flags: a comma-delimited list of strings representing the options used. May include: utf8 (this will always be set for new revisions); gzip; external. -'SavePreferences': called at the end of PreferencesForm::savePreferences; - returning false prevents the preferences from being saved. -$form: the PreferencesForm -$user: the User object to save preferences to -$message: change this to set an error message (ignored if the hook does not return false) -$old: old preferences of the user - 'SearchUpdate': Prior to search update completion $id : Page id $namespace : Page namespace $title : Page title $text : Current text being indexed -'SearchGetNearMatch': An extra chance for exact-title-matches in "go" searches +'SearchGetNearMatchBefore': Perform exact-title-matches in "go" searches before the normal operations +$allSearchTerms : Array of the search terms in all content languages +&$titleResult : Outparam; the value to return. A Title object or null. + +'SearchGetNearMatch': An extra chance for exact-title-matches in "go" searches if nothing was found $term : Search term string &$title : Outparam; set to $title object and return false for a match +'SearchGetNearMatchComplete': A chance to modify exact-title-matches in "go" searches +$term : Search term string +&$title : Current Title object that is being returned (null if none found). + +'SearchEngineReplacePrefixesComplete': Run after SearchEngine::replacePrefixes(). +$searchEngine : The SearchEngine object. Users of this hooks will be interested +in the $searchEngine->namespaces array. +$query : Original query. +&$parsed : Resultant query with the prefixes stripped. + +'SearchableNamespaces': An option to modify which namespaces are searchable. +&$arr : Array of namespaces ($nsId => $name) which will be used. + 'SetupAfterCache': Called in Setup.php, after cache objects are set +'ShowMissingArticle': Called when generating the output for a non-existent page +$article: The article object corresponding to the page + 'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views $text: Text being shown $title: Title of the custom script/stylesheet page $output: Current OutputPage object +'ShowSearchHitTitle': Customise display of search hit title/link. +&$title: Title to link to +&$text: Text to use for the link +$result: The search result +$terms: The search terms entered +$page: The SpecialSearch object. + 'SiteNoticeBefore': Before the sitenotice/anonnotice is composed &$siteNotice: HTML returned as the sitenotice Return true to allow the normal method of notice selection/rendering to work, @@ -1162,17 +1359,27 @@ $skin: Skin object &$bar: Sidebar contents Modify $bar to add or modify sidebar portlets. +'SkinCopyrightFooter': Allow for site and per-namespace customization of copyright notice. +$title: displayed page title +$type: 'normal' or 'history' for old/diff views +&$msg: overridable message; usually 'copyright' or 'history_copyright'. This message must be in HTML format, not wikitext! +&$link: overridable HTML link to be passed into the message as $1 + 'SkinSubPageSubtitle': At the beginning of Skin::subPageSubtitle() $skin: Skin object &$subpages: Subpage links HTML -If false is returned $subpages will be used instead of the HTML subPageSubtitle() generates. -If true is returned, $subpages will be ignored and the rest of subPageSubtitle() will run. +If false is returned $subpages will be used instead of the HTML +subPageSubtitle() generates. +If true is returned, $subpages will be ignored and the rest of +subPageSubtitle() will run. -'SkinTemplateBuildContentActionUrlsAfterSpecialPage': after the single tab when showing a special page +'SkinTemplateBuildContentActionUrlsAfterSpecialPage': after the single tab +when showing a special page $sktemplate: SkinTemplate object $content_actions: array of tabs -'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink': after creating the "permanent link" tab +'SkinTemplateBuildNavUrlsNav_urlsAfterPermalink': after creating the +"permanent link" tab $sktemplate: SkinTemplate object $nav_urls: array of tabs @@ -1181,7 +1388,13 @@ $nav_urls: array of tabs [See http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/examples/Content_action.php for an example] -'SkinTemplateOutputPageBeforeExec': Before SkinTemplate::outputPage() starts page output +'SkinTemplateNavigation': Alter the structured navigation links in SkinTemplates +&$sktemplate: SkinTemplate object +&$links: Structured navigation links +This is used to alter the navigation for skins which use buildNavigationUrls such as Vector. + +'SkinTemplateOutputPageBeforeExec': Before SkinTemplate::outputPage() +starts page output &$sktemplate: SkinTemplate object &$tpl: Template engine object @@ -1193,44 +1406,56 @@ $res: set to true to prevent active tabs $out: Css to return 'SkinTemplateTabAction': Override SkinTemplate::tabAction(). - You can either create your own array, or alter the parameters for the normal one. -&$this: The SkinTemplate instance. -$title: Title instance for the page. -$message: Visible label of tab. -$selected: Whether this is a selected tab. +You can either create your own array, or alter the parameters for +the normal one. +&$sktemplate: The SkinTemplate instance. +$title: Title instance for the page. +$message: Visible label of tab. +$selected: Whether this is a selected tab. $checkEdit: Whether or not the action=edit query should be added if appropriate. -&$classes: Array of CSS classes to apply. -&$query: Query string to add to link. -&$text: Link text. -&$result: Complete assoc. array if you want to return true. +&$classes: Array of CSS classes to apply. +&$query: Query string to add to link. +&$text: Link text. +&$result: Complete assoc. array if you want to return true. 'SkinTemplateTabs': called when finished to build the actions tabs $sktemplate: SkinTemplate object $content_actions: array of tabs -'SkinTemplateToolboxEnd': Called by SkinTemplate skins after toolbox links have been rendered (useful for adding more) +'SkinTemplateToolboxEnd': Called by SkinTemplate skins after toolbox links have +been rendered (useful for adding more) $tools: array of tools +'SoftwareInfo': Called by Special:Version for returning information about +the software +$software: The array of software in format 'name' => 'version'. + See SpecialVersion::softwareInformation() + 'SpecialContributionsBeforeMainOutput': Before the form on Special:Contributions $id: User identifier -'SpecialListusersDefaultQuery': called right before the end of UsersPager::getDefaultQuery() +'SpecialListusersDefaultQuery': called right before the end of +UsersPager::getDefaultQuery() $pager: The UsersPager instance $query: The query array to be returned -'SpecialListusersFormatRow': called right before the end of UsersPager::formatRow() +'SpecialListusersFormatRow': called right before the end of +UsersPager::formatRow() $item: HTML to be returned. Will be wrapped in
  • after the hook finishes $row: Database row object -'SpecialListusersHeader': called before closing the
    in UsersPager::getPageHeader() +'SpecialListusersHeader': called before closing the
    in +UsersPager::getPageHeader() $pager: The UsersPager instance $out: The header HTML -'SpecialListusersHeaderForm': called before adding the submit button in UsersPager::getPageHeader() +'SpecialListusersHeaderForm': called before adding the submit button in +UsersPager::getPageHeader() $pager: The UsersPager instance $out: The header HTML -'SpecialListusersQueryInfo': called right before the end of UsersPager::getQueryInfo() +'SpecialListusersQueryInfo': called right before the end of +UsersPager::getQueryInfo() $pager: The UsersPager instance $query: The query array to be returned @@ -1239,31 +1464,59 @@ $movePage: MovePageForm object $oldTitle: old title (object) $newTitle: new title (object) -'SpecialPage_initList': called when setting up SpecialPage::$mList, use this hook to remove a core special page +'SpecialPage_initList': called when setting up SpecialPage::$mList, use this +hook to remove a core special page $list: list (array) of core special pages -'SpecialRecentChangesPanel': called when building form options in SpecialRecentChanges +'SpecialRandomGetRandomTitle': called during the execution of Special:Random, +use this to change some selection criteria or substitute a different title +&$randstr: The random number from wfRandom() +&$isRedir: Boolean, whether to select a redirect or non-redirect +&$namespaces: An array of namespace indexes to get the title from +&$extra: An array of extra SQL statements +&$title: If the hook returns false, a Title object to use instead of the +result from the normal query + +'SpecialRecentChangesPanel': called when building form options in +SpecialRecentChanges &$extraOpts: array of added items, to which can be added $opts: FormOptions for this request -'SpecialRecentChangesQuery': called when building sql query for SpecialRecentChanges +'SpecialRecentChangesQuery': called when building sql query for +SpecialRecentChanges &$conds: array of WHERE conditionals for query &$tables: array of tables to be queried &$join_conds: join conditions for the tables $opts: FormOptions for this request +&$query_options: array of options for the database request -'SpecialSearchNogomatch': called when user clicked the "Go" button but the target doesn't exist +'SpecialSearchNogomatch': called when user clicked the "Go" button but the +target doesn't exist $title: title object generated from the text entred by the user -'SpecialSearchResults': called before search result display when there are matches +'SpecialSearchProfiles': allows modification of search profiles +&$profiles: profiles, which can be modified. + +'SpecialSearchResults': called before search result display when there +are matches $term: string of search term &$titleMatches: empty or SearchResultSet object &$textMatches: empty or SearchResultSet object -'SpecialSearchNoResults': called before search result display when there are no matches +'SpecialSearchNoResults': called before search result display when there are +no matches $term: string of search term -'SpecialVersionExtensionTypes': called when generating the extensions credits, use this to change the tables headers +'SpecialStatsAddExtra': add extra statistic at the end of Special:Statistics +&$extraStats: Array to save the new stats + ( $extraStats[''] => ; ) + +'SpecialUploadComplete': Called after successfully uploading a file from +Special:Upload +$form: The SpecialUpload object + +'SpecialVersionExtensionTypes': called when generating the extensions credits, +use this to change the tables headers $extTypes: associative array of extensions types 'SpecialWatchlistQuery': called when building sql query for SpecialWatchlist @@ -1272,10 +1525,16 @@ $extTypes: associative array of extensions types &$join_conds: join conditions for the tables &$fields: array of query fields -'TitleArrayFromResult': called when creating an TitleArray object from a database result +'TitleArrayFromResult': called when creating an TitleArray object from a +database result &$titleArray: set this to an object to override the default object returned $res: database result used to create the object +'TitleGetRestrictionTypes': Allows extensions to modify the types of protection + that can be applied. +$title: The title in question. +&$types: The types of protection available. + 'TitleMoveComplete': after moving an article (title) $old: old title $nt: new title @@ -1306,61 +1565,87 @@ $article: article object that was watched 'UploadForm:initial': before the upload form is generated $form: UploadForm object -You might set the member-variables $uploadFormTextTop and +You might set the member-variables $uploadFormTextTop and $uploadFormTextAfterSummary to inject text (HTML) either before or after the editform. -'UploadForm:BeforeProcessing': at the beginning of processUpload() +'UploadForm:BeforeProcessing': DEPRECATED! at the beginning of processUpload() $form: UploadForm object Lets you poke at member variables like $mUploadDescription before the file is saved. +'UploadCreateFromRequest': when UploadBase::createFromRequest has been called +$type: (string) the requested upload type +&$className: the class name of the Upload instance to be created + +'UploadComplete': when Upload completes an upload +&$upload: an UploadBase child instance + +'UploadFormInitDescriptor': after the descriptor for the upload form as been + assembled +$descriptor: (array) the HTMLForm descriptor + +'UploadFormSourceDescriptors': after the standard source inputs have been +added to the descriptor +$descriptor: (array) the HTMLForm descriptor + 'UploadVerification': additional chances to reject an uploaded file string $saveName: destination file name string $tempName: filesystem path to the temporary file for checks -string &$error: output: HTML error to show if upload canceled by returning false +string &$error: output: message key for message to show if upload canceled + by returning false. May also be an array, where the first element + is the message key and the remaining elements are used as parameters to + the message. 'UploadComplete': Upon completion of a file upload -$uploadForm: Upload form object. File can be accessed by $uploadForm->mLocalFile. +$uploadBase: UploadBase (or subclass) object. File can be accessed by + $uploadBase->getLocalFile(). -'User::mailPasswordInternal': before creation and mailing of a user's new temporary password +'User::mailPasswordInternal': before creation and mailing of a user's new +temporary password $user: the user who sent the message out $ip: IP of the user who sent the message out $u: the account whose new password will be set -'UserArrayFromResult': called when creating an UserArray object from a database result +'UserArrayFromResult': called when creating an UserArray object from a +database result &$userArray: set this to an object to override the default object returned $res: database result used to create the object 'userCan': To interrupt/advise the "user can do X to Y article" check. - If you want to display an error message, try getUserPermissionsErrors. +If you want to display an error message, try getUserPermissionsErrors. $title: Title object being checked against $user : Current user object $action: Action being checked $result: Pointer to result returned if hook returns false. If null is returned, - userCan checks are continued by internal code. + userCan checks are continued by internal code. 'UserCanSendEmail': To override User::canSendEmail() permission check $user: User (object) whose permission is being checked &$canSend: bool set on input, can override on output - -'UserClearNewTalkNotification': called when clearing the "You have new messages!" message, return false to not delete it +'UserClearNewTalkNotification': called when clearing the +"You have new messages!" message, return false to not delete it $user: User (object) that'll clear the message -'UserComparePasswords': called when checking passwords, return false to override the default password checks +'UserComparePasswords': called when checking passwords, return false to +override the default password checks &$hash: String of the password hash (from the database) &$password: String of the plaintext password the user entered -&$userId: Integer of the user's ID or Boolean false if the user ID was not supplied -&$result: If the hook returns false, this Boolean value will be checked to determine if the password was valid +&$userId: Integer of the user's ID or Boolean false if the user ID was not + supplied +&$result: If the hook returns false, this Boolean value will be checked to + determine if the password was valid 'UserCreateForm': change to manipulate the login form $template: SimpleTemplate instance for the form -'UserCryptPassword': called when hashing a password, return false to implement your own hashing method +'UserCryptPassword': called when hashing a password, return false to implement +your own hashing method &$password: String of the plaintext password to encrypt &$salt: String of the password salt or Boolean false if no salt is provided -&$wgPasswordSalt: Boolean of whether the salt is used in the default hashing method +&$wgPasswordSalt: Boolean of whether the salt is used in the default + hashing method &$hash: If the hook returns false, this String will be used as the hash 'UserEffectiveGroups': Called in User::getEffectiveGroups() @@ -1374,25 +1659,39 @@ $user: User to get groups for $user: User object &$email: email, change this to override local email -'UserGetEmailAuthenticationTimestamp': called when getting the timestamp of email authentification +'UserGetEmailAuthenticationTimestamp': called when getting the timestamp of +email authentification $user: User object -&$timestamp: timestamp, change this to override local email authentification timestamp +&$timestamp: timestamp, change this to override local email authentification + timestamp 'UserGetImplicitGroups': Called in User::getImplicitGroups() &$groups: List of implicit (automatically-assigned) groups 'UserGetReservedNames': allows to modify $wgReservedUsernames at run time -*&$reservedUsernames: $wgReservedUsernames +&$reservedUsernames: $wgReservedUsernames 'UserGetRights': Called in User::getRights() $user: User to get rights for &$rights: Current rights +'UserIsBlockedFrom': Check if a user is blocked from a specific page (for specific block + exemptions). +$user: User in question +$title: Title of the page in question +&$blocked: Out-param, whether or not the user is blocked from that page. +&$allowUsertalk: If the user is blocked, whether or not the block allows users to edit their + own user talk pages. + 'UserIsBlockedGlobally': Check if user is blocked on all wikis. &$user: User object $ip: User's IP address &$blocked: Whether the user is blocked, to be modified by the hook +'UserLoadAfterLoadFromSession': called to authenticate users on +external/environmental means; occurs after session is loaded +$user: user object being loaded + 'UserLoadDefaults': called when loading a default user $user: user object $name: user name @@ -1401,23 +1700,31 @@ $name: user name $user: user object &$s: database query object -'UserLoadFromSession': called to authenticate users on external/environmental means; occurs before session is loaded +'UserLoadFromSession': called to authenticate users on external/environmental +means; occurs before session is loaded $user: user object being loaded -&$result: set this to a boolean value to abort the normal authentification process +&$result: set this to a boolean value to abort the normal authentification + process -'UserLoadAfterLoadFromSession': called to authenticate users on external/environmental means; occurs after session is loaded -$user: user object being loaded +'UserLoadOptions': when user options/preferences are being loaded from +the database. +$user: User object +&$options: Options, can be modified. 'UserLoginComplete': after a user has logged in $user: the user object that was created on login $inject_html: Any HTML to inject after the "logged in" message. - + 'UserLoginForm': change to manipulate the login form $template: SimpleTemplate instance for the form +'UserLoginMailPassword': Block users from emailing passwords +$name: the username to email the password of. +&$error: out-param - the error message to return. + 'UserLogout': before a user logs out $user: the user object that is about to be logged out - + 'UserLogoutComplete': after a user has logged out $user: the user object _after_ logout (won't have name, ID, etc.) $inject_html: Any HTML to inject after the "logged out" message. @@ -1428,24 +1735,18 @@ $user : User object that was changed $add : Array of strings corresponding to groups added $remove: Array of strings corresponding to groups removed -'UserrightsChangeableGroups': allows modification of the groups a user may add or remove via Special:UserRights -$userrights : UserrightsPage object -$user : User object of the current user -$addergroups : Array of groups that the user is in -&$groups : Array of groups that can be added or removed. In format of - array( - 'add' => array( addablegroups ), - 'remove' => array( removablegroups ), - 'add-self' => array( addablegroups to self ), - 'remove-self' => array( removable groups from self ) - ) -'UserRetrieveNewTalks': called when retrieving "You have new messages!" message(s) +'UserRetrieveNewTalks': called when retrieving "You have new messages!" +message(s) $user: user retrieving new talks messages $talks: array of new talks page(s) 'UserSaveSettings': called when saving user settings $user: User object +'UserSaveOptions': Called just before saving user preferences/options. +$user: User object +&$options: Options, modifiable + 'UserSetCookies': called when setting user cookies $user: User object &$session: session array, will be added to $_SESSION @@ -1455,14 +1756,18 @@ $user: User object $user: User object &$email: new email, change this to override new email address -'UserSetEmailAuthenticationTimestamp': called when setting the timestamp of email authentification +'UserSetEmailAuthenticationTimestamp': called when setting the timestamp +of email authentification $user: User object -&$timestamp: new timestamp, change this to override local email authentification timestamp +&$timestamp: new timestamp, change this to override local email +authentification timestamp -'UserToggles': called when initialising User::$mToggles, use this to add new toggles +'UserToggles': called when initialising User::$mToggles, use this to add +new toggles $toggles: array of toggles to add -'WantedPages::getSQL': called in WantedPagesPage::getSQL(), can be used to alter the SQL query which gets the list of wanted pages +'WantedPages::getSQL': called in WantedPagesPage::getSQL(), can be used to +alter the SQL query which gets the list of wanted pages &$wantedPages: WantedPagesPage object &$sql: raw SQL query used to get the list of wanted pages @@ -1474,8 +1779,30 @@ $article: article object to be watched $user: user that watched $article: article object watched -'wgQueryPages': called when initialising $wgQueryPages, use this to add new query pages to be updated with maintenance/updateSpecialPages.php +'WikiExporter::dumpStableQuery': Get the SELECT query for "stable" revisions +dumps +One, and only one hook should set this, and return false. +&$tables: Database tables to use in the SELECT query +&$opts: Options to use for the query +&$join: Join conditions + +'wgQueryPages': called when initialising $wgQueryPages, use this to add new +query pages to be updated with maintenance/updateSpecialPages.php $query: $wgQueryPages itself +'XmlDumpWriterOpenPage': Called at the end of XmlDumpWriter::openPage, to allow extra + metadata to be added. +$obj: The XmlDumpWriter object. +&$out: The output string. +$row: The database row for the page. +$title: The title of the page. + +'XmlDumpWriterWriteRevision': Called at the end of a revision in an XML dump, to add extra + metadata. +$obj: The XmlDumpWriter object. +&$out: The text being output. +$row: The database row for the revision. +$text: The revision text. + More hooks might be available but undocumented, you can execute ./maintenance/findhooks.php to find hidden one. diff --git a/docs/maintenance.txt b/docs/maintenance.txt new file mode 100644 index 00000000..039c71c5 --- /dev/null +++ b/docs/maintenance.txt @@ -0,0 +1,57 @@ +Prior to version 1.16, maintenance scripts were a hodgepodge of code that +had no cohesion or formal method of action. Beginning in 1.16, maintenance +scripts have been cleaned up to use a unified class. + +1. Directory structure +2. How to run a script +3. How to write your own + +1. DIRECTORY STRUCTURE + The /maintenance directory of a MediaWiki installation contains several +subdirectories, all of which have unique purposes. + +2. HOW TO RUN A SCRIPT + Ridiculously simple, just call 'php someScript.php' that's in the top- +level /maintenance directory. + +Example: + php clear_stats.php + +The following parameters are available to all maintenance scripts +--help : Print a help message +--quiet : Quiet non-error output +--dbuser : The database user to use for the script (if needed) +--dbpass : Same as above (if needed) +--conf : Location of LocalSettings.php, if not default +--wiki : For specifying the wiki ID +--batch-size : If the script supports batch operations, do this many per batch + +3. HOW TO WRITE YOUR OWN +Make a file in the maintenance directory called myScript.php or something. +In it, write the following: + +==BEGIN== + +) - -Release 1.0.6 -------------- -* removed all array_push() calls -* applied patch provided by Stuart Herbert - corrects possible endless loop. Available at - http://bugs.gentoo.org/show_bug.cgi?id=25385 -* fixed problem with storing large binary files -* added more error checking, specifically on all socket functions -* added support for the INCR and DECR commands - which increment or decrement a value stored in MemCached -* Documentation removed from source and is now available - in the file Documentation + 1812 UTC: + Fixed memcached::enable_compression; + thanks to Justin Matlock for pointing it out + +07 Oct 2003: + 1635 UTC: + Fixed call to memcached::_dead_sock in memcached::delete + Added documentation for class variable $_buckets + +06 Oct 2003: + 2039 UTC: + Initial release of memcached-client-php; version 0.1 -Release 1.0.4 -------------- -* initial release, version numbers kept - in sync with MemCached version -* capable of storing any datatype in MemCached diff --git a/docs/php-memcached/README b/docs/php-memcached/README new file mode 100644 index 00000000..07812dda --- /dev/null +++ b/docs/php-memcached/README @@ -0,0 +1 @@ +HTML documentation is under http://phpca.cytherianage.net/memcached/doc/ diff --git a/docs/scripts.txt b/docs/scripts.txt index f8228a46..2027d176 100644 --- a/docs/scripts.txt +++ b/docs/scripts.txt @@ -35,10 +35,9 @@ Primary scripts: to force the profiler to save the informations in the database and apply the maintenance/archives/patch-profiling.sql patch to the database. - To enable the profileinfo.php itself, you'll need to create the - AdminSettings.php file (see AdminSettings.sample for more information) and - set $wgEnableProfileInfo to true in that file. See also - http://www.mediawiki.org/wiki/How_to_debug#Profiling. + To enable the profileinfo.php itself, you'll need to set $wgDBadminuser + and $wgDBadminpassword in your LocalSettings.php, as well as $wgEnableProfileInfo + See also http://www.mediawiki.org/wiki/How_to_debug#Profiling. redirect.php Script that only redirect to the article passed in the wpDropdown parameter diff --git a/docs/skin.txt b/docs/skin.txt index 524a0397..a42369ce 100644 --- a/docs/skin.txt +++ b/docs/skin.txt @@ -47,7 +47,7 @@ These can also be customised on a per-user basis, by editing This feature has led to a wide variety of "user styles" becoming available, which change the appearance of Monobook or MySkin: -http://meta.wikimedia.org/wiki/Gallery_of_user_styles +http://www.mediawiki.org/wiki/Manual:Gallery_of_user_styles If you want a different look for your wiki, that gallery is a good place to start. diff --git a/docs/upload.txt b/docs/upload.txt index e92ca786..a0f0a594 100644 --- a/docs/upload.txt +++ b/docs/upload.txt @@ -1,40 +1,2 @@ -Special:Upload: - -wfSpecialUpload - new UploadForm - mUpload = new UploadFrom... - execute() - $wgEnableUploads - isAllowed(upload) - isBlocked() - wfReadOnly() - processUpload() - internalProcessUpload() - wfRunHooks(UploadForm:BeforeProcessing) - mUpload->getTitle() - wfStripIllegalFilenameChars - splitExtensions() - checkFileExtension() - Title::makeTitleSafe - getUserPermissionsErrors(edit; upload; create) - mUpload->verifyUpload() - empty(mFileSize) - getTitle() - checkOverwrite() - verifyFile() - checkMacBinary() - wfRunHooks(UploadVerification) - if(!ignoreWarning) mUpload->checkWarnings() - getInitialPageText() - mUpload->performUpload() - mLocalFile->upload() - if(isGood() && $watch) addWatch() - if(isGood()) wfRunHooks(UploadComplete) - wfRunHooks(SpecialUploadComplete) - -Changes: - * "Your file will be renamed to $1" check now done on the result of - Title::makeTitleSafe instead of filteredName - * getExistWarning only really does existence checks - * Other stuff forgotten to be documented - \ No newline at end of file +This document describes how the current uploading system is build up and how +custom backends can be built. (At least someday it will). diff --git a/extensions/FluxBBAuthPlugin.php b/extensions/FluxBBAuthPlugin.php index 0b1319eb..17bc2f24 100644 --- a/extensions/FluxBBAuthPlugin.php +++ b/extensions/FluxBBAuthPlugin.php @@ -4,7 +4,7 @@ $wgHooks['isValidPassword'][] = 'FluxBBAuthPlugin::isValidPassword'; $wgExtensionCredits['other'][] = array( 'name' => 'FluxBBAuthPlugin', - 'version' => '1.0', + 'version' => '1.1', 'description' => 'Use FluxBB accounts in MediaWiki', 'author' => 'Pierre Schmitz', 'url' => 'https://users.archlinux.de/~pierre/' @@ -58,7 +58,7 @@ public function authenticate( $username, $password ) { return $authenticated; } -public function modifyUITemplate( &$template ) { +public function modifyUITemplate( &$template, &$type ) { $template->set( 'usedomain', false ); $template->set('link', 'Um Dich hier anzumelden, nutze Deine Konto-Daten aus dem archlinux.de-Forum.'); } @@ -79,6 +79,17 @@ public function autoCreate() { return true; } +public function allowPropChange( $prop = '' ) { + if( $prop == 'realname' ) { + return false; + } elseif( $prop == 'emailaddress' ) { + return false; + } elseif( $prop == 'nickname' ) { + return false; + } else { + return true; + } +} public function allowPasswordChange() { return false; } diff --git a/img_auth.php b/img_auth.php index e5d8d888..bc4464d4 100644 --- a/img_auth.php +++ b/img_auth.php @@ -3,118 +3,112 @@ /** * Image authorisation script * - * To use this: + * To use this, see http://www.mediawiki.org/wiki/Manual:Image_Authorization * * - Set $wgUploadDirectory to a non-public directory (not web accessible) * - Set $wgUploadPath to point to this file * - * Your server needs to support PATH_INFO; CGI-based configurations - * usually don't. + * Optional Parameters + * + * - Set $wgImgAuthDetails = true if you want the reason the access was denied messages to be displayed + * instead of just the 403 error (doesn't work on IE anyway), otherwise will only appear in error logs + * - Set $wgImgAuthPublicTest false if you don't want to just check and see if all are public + * must be set to false if using specific restrictions such as LockDown or NSFileRepo + * + * For security reasons, you usually don't want your user to know *why* access was denied, just that it was. + * If you want to change this, you can set $wgImgAuthDetails to 'true' in localsettings.php and it will give the user the reason + * why access was denied. + * + * Your server needs to support PATH_INFO; CGI-based configurations usually don't. * * @file - */ - + * + **/ + define( 'MW_NO_OUTPUT_COMPRESSION', 1 ); require_once( dirname( __FILE__ ) . '/includes/WebStart.php' ); wfProfileIn( 'img_auth.php' ); require_once( dirname( __FILE__ ) . '/includes/StreamFile.php' ); -$perms = User::getGroupPermissions( array( '*' ) ); -if ( in_array( 'read', $perms, true ) ) { - wfDebugLog( 'img_auth', 'Public wiki' ); - wfPublicError(); +// See if this is a public Wiki (no protections) +if ( $wgImgAuthPublicTest + && in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) ) +{ + wfForbidden('img-auth-accessdenied','img-auth-public'); } // Extract path and image information -if( !isset( $_SERVER['PATH_INFO'] ) ) { - wfDebugLog( 'img_auth', 'Missing PATH_INFO' ); - wfForbidden(); -} +if( !isset( $_SERVER['PATH_INFO'] ) ) + wfForbidden('img-auth-accessdenied','img-auth-nopathinfo'); $path = $_SERVER['PATH_INFO']; $filename = realpath( $wgUploadDirectory . $_SERVER['PATH_INFO'] ); $realUpload = realpath( $wgUploadDirectory ); -wfDebugLog( 'img_auth', "\$path is {$path}" ); -wfDebugLog( 'img_auth', "\$filename is {$filename}" ); // Basic directory traversal check -if( substr( $filename, 0, strlen( $realUpload ) ) != $realUpload ) { - wfDebugLog( 'img_auth', 'Requested path not in upload directory' ); - wfForbidden(); -} +if( substr( $filename, 0, strlen( $realUpload ) ) != $realUpload ) + wfForbidden('img-auth-accessdenied','img-auth-notindir'); // Extract the file name and chop off the size specifier // (e.g. 120px-Foo.png => Foo.png) $name = wfBaseName( $path ); if( preg_match( '!\d+px-(.*)!i', $name, $m ) ) $name = $m[1]; -wfDebugLog( 'img_auth', "\$name is {$name}" ); + +// Check to see if the file exists +if( !file_exists( $filename ) ) + wfForbidden('img-auth-accessdenied','img-auth-nofile',$filename); + +// Check to see if tried to access a directory +if( is_dir( $filename ) ) + wfForbidden('img-auth-accessdenied','img-auth-isdir',$filename); + $title = Title::makeTitleSafe( NS_FILE, $name ); -if( !$title instanceof Title ) { - wfDebugLog( 'img_auth', "Unable to construct a valid Title from `{$name}`" ); - wfForbidden(); -} -$title = $title->getPrefixedText(); -// Check the whitelist if needed -if( !$wgUser->getId() && ( !is_array( $wgWhitelistRead ) || !in_array( $title, $wgWhitelistRead ) ) ) { - wfDebugLog( 'img_auth', "Not logged in and `{$title}` not in whitelist." ); - wfForbidden(); -} +// See if could create the title object +if( !$title instanceof Title ) + wfForbidden('img-auth-accessdenied','img-auth-badtitle',$name); -if( !file_exists( $filename ) ) { - wfDebugLog( 'img_auth', "`{$filename}` does not exist" ); - wfForbidden(); -} -if( is_dir( $filename ) ) { - wfDebugLog( 'img_auth', "`{$filename}` is a directory" ); - wfForbidden(); -} +// Run hook +if (!wfRunHooks( 'ImgAuthBeforeStream', array( &$title, &$path, &$name, &$result ) ) ) + wfForbidden($result[0],$result[1],array_slice($result,2)); + +// Check user authorization for this title +// UserCanRead Checks Whitelist too +if( !$title->userCanRead() ) + wfForbidden('img-auth-accessdenied','img-auth-noread',$name); // Stream the requested file -wfDebugLog( 'img_auth', "Streaming `{$filename}`" ); +wfDebugLog( 'img_auth', "Streaming `".$filename."`." ); wfStreamFile( $filename, array( 'Cache-Control: private', 'Vary: Cookie' ) ); wfLogProfilingData(); /** - * Issue a standard HTTP 403 Forbidden header and a basic - * error message, then end the script + * Issue a standard HTTP 403 Forbidden header ($msg1-a message index, not a message) and an + * error message ($msg2, also a message index), (both required) then end the script + * subsequent arguments to $msg2 will be passed as parameters only for replacing in $msg2 */ -function wfForbidden() { +function wfForbidden($msg1,$msg2) { + global $wgImgAuthDetails; + $args = func_get_args(); + array_shift( $args ); + array_shift( $args ); + $MsgHdr = htmlspecialchars(wfMsg($msg1)); + $detailMsg = (htmlspecialchars(wfMsg(($wgImgAuthDetails ? $msg2 : 'badaccess-group0'),$args))); + wfDebugLog('img_auth', "wfForbidden Hdr:".wfMsgExt( $msg1, array('language' => 'en'))." Msg: ". + wfMsgExt($msg2,array('language' => 'en'),$args)); header( 'HTTP/1.0 403 Forbidden' ); - header( 'Vary: Cookie' ); + header( 'Cache-Control: no-cache' ); header( 'Content-Type: text/html; charset=utf-8' ); echo << -

    Access Denied

    -

    You need to log in to access files on this server.

    +

    $MsgHdr

    +

    $detailMsg

    ENDS; wfLogProfilingData(); exit(); } - -/** - * Show a 403 error for use when the wiki is public - */ -function wfPublicError() { - header( 'HTTP/1.0 403 Forbidden' ); - header( 'Content-Type: text/html; charset=utf-8' ); - echo << - -

    Access Denied

    -

    The function of img_auth.php is to output files from a private wiki. This wiki -is configured as a public wiki. For optimal security, img_auth.php is disabled in -this case. -

    - - -ENDS; - wfLogProfilingData(); - exit; -} - 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 * '' 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 ''; } - if('w' !== $watch && 'u' !== $watch) { + if ( 'w' !== $watch && 'u' !== $watch ) { return ''; } $watch = 'w' === $watch; - $title = Title::newFromDBkey($pagename); - if(!$title) { + $title = Title::newFromDBkey( $pagename ); + if ( !$title ) { // Invalid title return ''; } - $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 ''; } - if( $watch ) { - return ''.wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() ); + if ( $watch ) { + return '' . wfMsgExt( 'addedwatchtext', array( 'parse' ), $title->getPrefixedText() ); } else { - return ''.wfMsgExt( 'removedwatchtext', array( 'parse' ), $title->getPrefixedText() ); + return '' . 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 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 @@ -1080,6 +1329,65 @@ class Article { && !$this->mTitle->isCssJsSubpage(); } + /** + * 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 @@ -1087,39 +1395,51 @@ class Article { * @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() ? '←' : '→'; // 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,12 +117,31 @@ 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? * @@ -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 = ' ', $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.' '; + $r .= '<td class="mw-enhanced-rc">'.$tl.' '; # Main line $r .= $this->recentChangesFlags( $isnew, false, $unpatrolled, ' ', $bot ); # Timestamp - $r .= ' '.$block[0]->timestamp.' </tt></td><td>'; + $r .= ' '.$block[0]->timestamp.' </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, ' ', $rc_bot ); - $r .= ' </tt></td><td valign="top">'; + $r .= ' </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 '     '; @@ -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() . ' '; + $r = '<table class="mw-enhanced-rc"><tr>'; + $r .= '<td class="mw-enhanced-rc">' . $this->spacerArrow() . ' '; # Flag and Timestamp if( $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $r .= '    '; // 4 flags -> 4 spaces } else { $r .= $this->recentChangesFlags( $rc_type == RC_NEW, $rc_minor, $rcObj->unpatrolled, ' ', $rc_bot ); } - $r .= ' '.$rcObj->timestamp.' </tt></td><td>'; + $r .= ' '.$rcObj->timestamp.' </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; @@ -164,6 +165,16 @@ $wgTmpDirectory = false; ///< defaults to "{$wgUploadDirectory}/tmp" $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. */ @@ -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 @@ -236,6 +257,13 @@ $wgLocalFileRepo = false; $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 <html> 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 */ @@ -1045,6 +1221,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 */ @@ -1072,23 +1258,34 @@ $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... @@ -1799,6 +2037,18 @@ $wgAdvancedSearchHighlighting = false; $wgSearchHighlightBoundaries = version_compare("5.1", PHP_VERSION, "<")? '[\p{Z}\p{P}\p{C}]' : '[ ,.;:!?~!@#$%\^&*\(\)+=\-\\|\[\]"\'<>\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 * @@ -1813,9 +2063,22 @@ $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 * @@ -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; @@ -2250,6 +2535,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, @@ -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; @@ -2645,6 +2961,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, @@ -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', @@ -3087,14 +3411,6 @@ $wgSpecialPageGroups = array( 'Booksources' => 'other', ); -/** - * 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 @@ -3514,6 +3860,13 @@ $wgDjvuDump = null; # $wgDjvuRenderer = 'ddjvu'; $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. @@ -3581,6 +3934,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. @@ -3594,6 +3965,21 @@ $wgParserTestFiles = array( "$IP/maintenance/parserTests.txt", ); +/** + * 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. @@ -3651,6 +4037,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 @@ -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 @@ -3757,6 +4156,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 */ @@ -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*\"([^<]*?)\"\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 { @@ -536,14 +481,37 @@ 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 .= ' '; - $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 ) . - " <label for='wpMinoredit'".$skin->tooltip('minoredit', 'withaccess').">{$minorLabel}</label>"; + " <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 ) . - " <label for='wpWatchthis'".$skin->tooltip('watch', 'withaccess').">{$watchLabel}</label>"; + " <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. @@ -2434,59 +2539,23 @@ END $wgOut->addWikiMsg( 'nocreatetext' ); } - /** - * If there are rows in the deletion log for this page, show them, - * along with a nice little note for the user - * - * @param OutputPage $out - */ - 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 @@ -215,7 +220,7 @@ class MWException extends Exception { * print the end of the html page if not using $wgOut. */ function htmlFooter() { - echo ""; + return ""; } /** @@ -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 = ""; //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 = "\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 .= " "; 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 .= " \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 @@ 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 ) { ?> <?php print $item->getTitle() ?> getUrl() ?> + RSSIsPermalink ) print ' isPermaLink="true"' ?>>getUniqueId() ?> getDescription() ?> getDate() ) { ?>formatTime( $item->getDate() ) ?> getAuthor() ) { ?>getAuthor() ?> @@ -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; ?> - getUrl() ?> + getUniqueId() ?> <?php print $item->getTitle() ?> 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 @@ 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 = " - " . + " . Xml::checkLabel( wfMsg( 'revdelete-suppress' ), 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '3' ) ) . - " + " "; } else { $suppress = ''; @@ -173,14 +183,18 @@ class FileDeleteForm { array( 'type' => 'text', 'maxlength' => '255', 'tabindex' => '2', 'id' => 'wpReason' ) ) . " - {$suppress} + {$suppress}"; + if( $wgUser->isLoggedIn() ) { + $form .= " " . Xml::checkLabel( wfMsg( 'watchthis' ), 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) . " - + "; + } + $form .= " " . @@ -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 .= '

    ' . $link . '

    '; } @@ -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 @@ -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 ) { @@ -129,15 +261,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: * parse: parses wikitext to html * parseinline: 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( "", wfHostname(), $elapsed ) - : sprintf( "", $elapsed ); - } + return $wgShowHostnames + ? sprintf( "", wfHostname(), $elapsed ) + : sprintf( "", $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 @@ + $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 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 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 @@ 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() ) . ''; } elseif( $this->mHideBadImages && wfIsBadImage( $nt->getDBkey(), $this->getContextTitle() ) ) { # The image is blacklisted, just show it as a text link. - $thumbhtml = "\n\t\t\t".'
    ' - . $sk->makeKnownLinkObj( $nt, htmlspecialchars( $nt->getText() ) ) . '
    '; + $thumbhtml = "\n\t\t\t".'
    ' . + $sk->link( + $nt, + htmlspecialchars( $nt->getText() ), + array(), + array(), + array( 'known', 'noclasses' ) + ) . + '
    '; } elseif( !( $thumb = $img->transform( $params ) ) ) { # Error generating thumbnail. $thumbhtml = "\n\t\t\t".'
    ' . htmlspecialchars( $img->getLastError() ) . '
    '; } 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". '
    ' @@ -265,7 +281,7 @@ class ImageGallery # handlers since they may emit block-level elements as opposed to simple tags. # ref http://css-discuss.incutio.com/?page=CenteringBlockElement . '
    ' - . $thumb->toHtml( array( 'desc-link' => true, 'desc-query' => $descQuery ) ) . '
    '; + . $thumb->toHtml( $imageParameters ) . ''; // 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 ) ) ) . "
    \n" : + $sk->link( + $nt, + htmlspecialchars( $wgLang->truncate( $nt->getText(), 20 ) ), + array(), + array(), + array( 'known', 'noclasses' ) + ) . "
    \n" : '' ; # ATTENTION: The newline after
    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( '
    ' . $this->mExtraDescription . '
    ' ); + $wgOut->addHTML( '
    ' . $this->mExtraDescription . "
    \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 = ''; - return $r; + $r = array( + '
  • ' . wfMsgHtml( 'file-anchor-link' ) . '
  • ', + '
  • ' . wfMsgHtml( 'filehist' ) . '
  • ', + '
  • ' . wfMsgHtml( 'imagelinks' ) . '
  • ', + ); + if ( $metadata ) { + $r[] = '
  • ' . wfMsgHtml( 'metadata' ) . '
  • '; + } + + wfRunHooks( 'ImagePageShowTOC', array( $this, &$r ) ); + + return '
      ' . implode( "\n", $r ) . '
    '; } /** @@ -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 = "\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 { '
    ' . Xml::tags( 'a', $linkAttribs, $msgbig ) . "$dirmark " . $longDesc; } - if( $this->displayImg->isMultipage() ) { + $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1; + if( $isMulti ) { $wgOut->addHTML( '' - . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deleterevision') ) ? '' : '' ) + . ( $this->current->isLocal() && ($wgUser->isAllowed('delete') || $wgUser->isAllowed('deletedhistory') ) ? '' : '' ) . '' - . '' + . ( $this->showThumb ? '' : '' ) . '' . '' . '' @@ -763,7 +844,7 @@ class ImageHistoryList { } public function endImageHistoryList( $navLinks = '' ) { - return "
    ' ); } @@ -371,15 +386,21 @@ class ImagePage extends Article { ); $wgOut->addHTML( '' ); + $anchorclose . "\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( '' ); + $icon->toHtml( array( 'file-link' => true ) ) . + "\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( << -[[Media:$filename|$filename]]$dirmark - $longDesc +{$medialink}$dirmark +$longDesc -
    $warning
    EOT ); } else { $wgOut->addWikiText( << -[[Media:$filename|$filename]]$dirmark $longDesc +{$medialink}{$dirmark} +$longDesc 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( "", $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 = "
    \n$1\n
    \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 = "
    "; - $s .= wfMsgWikiHtml( 'sharedupload', $this->img->getRepo()->getDisplayName(), $msg ); - $s .= "
    "; - $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( '
      ' ); + $wgOut->addHTML( "
        \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( "
      • " ); + $wgOut->addHTML( "
      • {$ulink}
      • \n" ); } # External editing link - $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); - $wgOut->addHTML( '
      • ' . $elink . ' ' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '
      • ' ); + if ( $wgUseExternalEditor ) { + $elink = $sk->link( + $this->mTitle, + wfMsgHtml( 'edit-externally' ), + array(), + array( + 'action' => 'edit', + 'externaledit' => 'true', + 'mode' => 'file' + ), + array( 'known', 'noclasses' ) + ); + $wgOut->addHTML( '
      • ' . $elink . ' ' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . "
      • \n" ); + } - $wgOut->addHTML( '
      ' ); + $wgOut->addHTML( "
    \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( "
  • {$link}
  • \n" ); } } - $wgOut->addHTML( "\n" ); + $wgOut->addHTML( "\n" ); $res->free(); // Add a links to [[Special:Whatlinkshere]] if( $count > $limit ) $wgOut->addWikiMsg( 'morelinkstoimage', $this->mTitle->getPrefixedDBkey() ); + $wgOut->addHTML( "\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( "
  • {$link}
  • \n" ); } $wgOut->addHTML( "\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( "
    \n$1
    \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" + . "
    \n" . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) ) - . $navLinks - . Xml::openElement( 'table', array( 'class' => 'filehistory' ) ) . "\n" + . $navLinks . "\n" + . Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n" . '
    ' . wfMsgHtml( 'filehist-datetime' ) . '' . wfMsgHtml( 'filehist-thumb' ) . '' . wfMsgHtml( 'filehist-thumb' ) . '' . wfMsgHtml( 'filehist-dimensions' ) . '' . wfMsgHtml( 'filehist-user' ) . '' . wfMsgHtml( 'filehist-comment' ) . '
    \n$navLinks\n"; + return "\n$navLinks\n
    \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 .= ''; # 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 .= '
    '; + $row .= '
    '; } - $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 = "$del"; + 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 .= "$del"; + $row .= $del; } $row .= ''; } @@ -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 .= ''; @@ -847,43 +928,40 @@ class ImageHistoryList { if( !$file->userCan(File::DELETED_FILE) ) { # Don't link to unviewable files $row .= '' . $wgLang->timeAndDate( $timestamp, true ) . ''; - } 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 .= ''.$url.''; } else { $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img ); $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeAndDate( $timestamp, true ) ); } + $row .= ""; // 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 .= '' . ( $thumbnail ? $thumbnail->toHtml( $options ) : - wfMsgHtml( 'filehist-nothumb' ) ); - } else { - $row .= '' . wfMsgHtml( 'filehist-nothumb' ); + if ( $this->showThumb ) { + $row .= '' . $this->getThumbForLine( $file ) . ''; } - $row .= ""; - - // Image dimensions - $row .= htmlspecialchars( $dims ); - // File size - $row .= " (" . $this->skin->formatSize( $size ) . ')'; + // Image dimensions + size + $row .= ''; + $row .= htmlspecialchars( $file->getDimensionsString() ); + $row .= " (" . $this->skin->formatSize( $file->getSize() ) . ')'; + $row .= ''; // Uploading user - $row .= ''; + $row .= ''; if( $local ) { // Hide deleted usernames if( $file->isDeleted(File::DELETED_USER) ) { @@ -910,6 +988,33 @@ class ImageHistoryList { return "{$row}\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 , 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 , got " ); @@ -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 appendfield}>, got " ); @@ -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 @@ + + * @copyright 2002 Douglas Crockford (jsmin.c) + * @copyright 2008 Ryan Grove (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( /*   */ "\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( /*   */ "\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 @@ -17,25 +17,15 @@ 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 ) { @@ -319,236 +324,6 @@ class Linker { return htmlspecialchars( $target->getPrefixedText() ); } - /** - * @deprecated Use link() - * - * This function is a shortcut to makeLinkObj(Title::newFromText($title),...). Do not call - * it if you already have a title object handy. See makeLinkObj for further documentation. - * - * @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,51 +397,12 @@ 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 = "
    {$s}
    "; } 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 = "
    "; 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 '' . $prefix . $text . $inside . '' . $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 "{$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 - * - * @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 ''.$text.''; } @@ -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 = " " . wfMsgHtml( 'rev-deleted-comment' ) . ""; } else if( $rev->userCan( Revision::DELETED_COMMENT ) ) { @@ -1401,12 +1262,16 @@ class Linker { return "$stxt"; } - /** @todo document */ + /** + * Add another level to the Table of Contents + */ function tocIndent() { return "\n
      "; } - /** @todo document */ + /** + * Finish one or more sublevels on the Table of Contents + */ function tocUnindent($level) { return "\n" . str_repeat( "
    \n\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
  • ' . $tocnumber . ' ' . $tocline . ''; } - /** @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 "
  • \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 - '\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 = ' '; } - $class = 'TablePager_col_' . htmlspecialchars( $field ); - $s .= "\n"; + $s .= Xml::tags( 'td', $this->getCellAttrs( $field, $value ), $formatted ); } $s .= "\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] = "\"$msgLabel\"/
    $msgLabel"; - $disabledTexts[$type] = "\"$msgLabel\"/
    $msgLabel"; + $linkTexts[$type] = "\"$msgLabel\"/
    $msgLabel"; + $disabledTexts[$type] = "\"$msgLabel\"/
    $msgLabel"; } $links = $this->getPagingLinks( $linkTexts, $disabledTexts ); @@ -832,10 +937,19 @@ abstract class TablePager extends IndexPager { function getLimitSelect() { global $wgLang; $s = ""; 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 - "" . + Xml::openElement( + 'form', + array( + 'method' => 'get', + 'action' => $wgScript + ) + ) . "\n" . wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) . "\n\n" . - $this->getHiddenFields( array('limit','title') ) . + $this->getHiddenFields( array( 'limit' ) ) . "\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 @@ +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 @@ + 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 ) ) . '
    '; + $disableEmailPrefs = false; + } else { + $disableEmailPrefs = true; + $skin = $wgUser->getSkin(); + $emailauthenticated = wfMsgExt( 'emailnotauthenticated', 'parseinline' ) . '
    ' . + $skin->link( + SpecialPage::getTitleFor( 'Confirmemail' ), + wfMsg( 'emailconfirmlink' ), + array(), + array(), + array( 'known', 'noclasses' ) + ) . '
    '; + } + } 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' => ' ', + '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' => ' ', + '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' => ' ', + '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[] = "$previewtext"; + + # 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
     or \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( "\n";
    -
    -		return implode( "\n\t\t", $r );
     	}
     
     	/**
     	 * Make a " );
    -		global $wgUseSiteJs;
    -		if ($wgUseSiteJs) {
    -			$jsCache = $wgUser->isLoggedIn() ? '&smaxage=0' : '';
    -			$r[] = "";
    -		}
    -		if( $allowUserJs && $wgUser->isLoggedIn() ) {
    -			$userpage = $wgUser->getUserPage();
    -			$userjs = htmlspecialchars( self::makeUrl(
    -				$userpage->getPrefixedText().'/'.$this->getSkinName().'.js',
    -				'action=raw&ctype='.$wgJsMimeType));
    -			$r[] = '";
    -		}
    -		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 .= <<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 = "/**/";
    +				$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 .= "
    '; + $this->logoText() . ''; } elseif( $left ) { $s .= $this->getQuickbarCompensator( $rows ); } - $l = $wgContLang->isRTL() ? 'right' : 'left'; + $l = $wgContLang->alignStart(); $s .= "\n"; + $s .= "\n
    " . $this->searchForm() . ''; if ( $langlinks ) { $s .= "\n\n\n"; @@ -744,25 +758,26 @@ END; $s .= "\n
    $notice
    \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 = ""; $pop = ''; @@ -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 .= ''; } @@ -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 .= "
    " . wfMsgExt( 'hidden-categories', array( 'parsemag', 'escapenoentities' ), count( $allCats['hidden'] ) ) . $colon . $embed . implode( "$pop $sep $embed", $allCats['hidden'] ) . $pop . - "
    "; + ''; } # optional 'dmoz-like' category browser. Will be shown under the list # of categories an article belong to - if( $wgUseCategoryBrowser ){ + if( $wgUseCategoryBrowser ) { $s .= '

    '; # 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("
    \n", $tempout); + $s .= implode( "
    \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 >, 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) . ' > '; + $return .= Skin::drawCategoryBrowser( $parent, $skin ) . ' > '; } # 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, ''; + } 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[] = ''. wfMsg( 'youhavenewmessages', $tl, $dl ) . ''; # 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[] = "" . wfMsg( 'printableversion' ) . ''; + } - $s[] = "" . wfMsg( 'printableversion' ) . ''; 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 = '

    ' . $wgOut->getPageTitle() . '

    '; @@ -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)?"

    $subpages":''; + $sub .= !empty( $subpages ) ? "

    $subpages" : ''; $s = "

    {$sub}

    \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 .= '< '; } @@ -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 = 'searchboxes . '" name="search" class="inline" method="post" action="' . $this->escapeSearchLink() . "\">\n" - . '\n" - . ''; - - if ($wgUseTwoButtonsSearchForm) - $s .= ' \n"; - else - $s .= ' \n"; - + . '\n" + . ''; + + if( $wgUseTwoButtonsSearchForm ) { + $s .= ' \n"; + } else { + $s .= ' \n"; + } + $s .= ''; - + // 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, '' . htmlspecialchars( $varname ) . '' ) ); + } + $s = $wgLang->pipeList( array( + $s, + '' . htmlspecialchars( $varname ) . '' + ) ); } } 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
    "; - 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 .= "
    \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 .= ''; } $text = htmlspecialchars( $wgRightsText ); - $out .= "$text"; + $out .= "\"$text\""; if ( $wgRightsUrl ) { $out .= ''; } @@ -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 = 'Powered by MediaWiki'; + $img = 'Powered by MediaWiki'; 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 = ""; @@ -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 "trackbackURL() . "\">" - . wfMsg('trackbacklink') . ""; + return '' + . wfMsg( 'trackbacklink' ) . ''; } function otherLanguages() { @@ -1680,35 +1875,41 @@ END; $s = wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' ); $first = true; - if($wgContLang->isRTL()) $s .= ''; + if( $wgContLang->isRTL() ) { + $s .= ''; + } 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 .= "{$text}"; } - if($wgContLang->isRTL()) $s .= ''; + if( $wgContLang->isRTL() ) { + $s .= ''; + } 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§ion=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 + * 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)? - ''.$subpagestr.''.$out->getSubtitle(): + 'subtitle', !empty( $subpagestr ) ? + ''.$subpagestr.''.$out->getSubtitle() : $out->getSubtitle() ); $undelete = $this->getUndeleteLink(); $tpl->set( - "undelete", !empty($undelete)? - ''.$undelete.'': + 'undelete', !empty( $undelete ) ? + ''.$undelete.'' : '' ); - $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§ion=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 = '/*getText('wpTextbox1') . ' /*]]>*/'; + $this->userjsprev = '/*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 @@ +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 @@ -472,18 +469,6 @@ class Title { return $name; } - /** - * 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 ) { @@ -1001,6 +984,19 @@ class Title { return false; } + /** + * 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,37 +1499,10 @@ 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__ @@ -2285,53 +2398,42 @@ 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 @@ -3232,6 +3324,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 @@ db = $db; $this->database = $database; @@ -13,15 +23,33 @@ class UserRightsProxy { $this->id = intval( $id ); } + /** + * 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( "
  • " . $skin->makeLinkObj( $title ) - . ' (' . $skin->makeLinkObj( $title->getTalkPage(), $talk ) . ")
  • \n" ); + $output->addHTML( "
  • " . $skin->link( $title ) + . ' (' . $skin->link( $title->getTalkPage(), $talk ) . ")
  • \n" ); } } $output->addHTML( "\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 .= "
    \n" . wfMsgHtml( 'watchlistedit-normal-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 = '' . $link . ''; - $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 "
  • " . 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 .= '
    ' . wfMsgHtml( 'watchlistedit-raw-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 @@ +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
  • + \n + \n + + \n + \n + \n + + \n + \n + + \n + + \n + " . + 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 = ''; - - $txt .= ' -
    ' + '
    ' . '

    ' . $title . "

    \n" . $toc # no trailing newline, script should not be wrapped in a # paragraph . "\n
    " - . '\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 = "" - . "$text" + . " $text" . ""; if ( $legacyAnchor !== false ) { - $ret = "$ret"; + $ret = "$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 = '
    '; 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 .= "
    $formatted
    \n" . - $this->logoText() . '\n"; - $s .= $this->topLinks() ; - $s .= "

    " . $this->pageTitleLinks() . "

    \n"; + $s .= $this->topLinks(); + $s .= '

    ' . $this->pageTitleLinks() . "

    \n"; - $r = $wgContLang->isRTL() ? "left" : "right"; + $r = $wgContLang->alignEnd(); $s .= "
    "; $s .= $this->nameAndLogin(); - $s .= "\n
    " . $this->searchForm() . "
    $langlinks
    + * @param $attribs An array of attributes to apply to the tr tag + * @param $cells An array of strings to put in * @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 @@ @gmail.com + * Copyright © 2006, 2010 Yuri Astrakhan @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 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
    Debugging value '$name':\n\n";
    -		var_export($value);
    -		if ($backtrace)
    +		var_export( $value );
    +		if ( $backtrace ) {
     			print "\n" . wfBacktrace();
    +		}
     		print "\n
    \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 @@ .@home.nl + * Copyright © 2007 Roan Kattouw .@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 @@ .@home.nl + * Copyright © 2007 Roan Kattouw .@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 @@ .@home.nl + * Copyright © 2008 Roan Kattouw .@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; } @@ -101,30 +102,40 @@ abstract class ApiFormatBase extends ApiBase { return $this->mIsHtml; } + /** + * 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 ) { ?> -mUnescapeAmps) { +mUnescapeAmps ) { ?> MediaWiki API MediaWiki API Result @@ -134,12 +145,12 @@ abstract class ApiFormatBase extends ApiBase { -
    +
    -You are looking at the HTML representation of the mFormat ); ?> format.
    -HTML is good for debugging, but probably is not suitable for your application.
    +You are looking at the HTML representation of the mFormat ); ?> format.
    +HTML is good for debugging, but probably is not suitable for your application.
    See complete documentation, or API help for more information.
    @@ -159,7 +170,7 @@ See complete documentation, or * Finish printing. Closes HTML tags. */ public function closePrinter() { - if ($this->getIsHtml()) { + if ( $this->getIsHtml() ) { ?> @@ -177,15 +188,16 @@ See complete documentation, 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; @@ -194,6 +206,19 @@ See complete documentation, 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 complete documentation, 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('/\<(!--.*?--|.*?)\>/', '<\1>', $text); + $text = preg_replace( '/\<(!--.*?--|.*?)\>/', '<\1>', $text ); // identify URLs - $protos = implode("|", $wgUrlProtocols); - # This regex hacks around bug 13218 (" included in the URL) - $text = preg_replace("#(($protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '\\1\\3\\4', $text); + $protos = implode( "|", $wgUrlProtocols ); + // This regex hacks around bug 13218 (" included in the URL) + $text = preg_replace( "#(($protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '\\1\\3\\4', $text ); // identify requests to api.php - $text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '\\0', $text); - if( $this->mHelp ) { + $text = preg_replace( "#api\\.php\\?[^ \\()<\n\t]+#", '\\0', $text ); + if ( $this->mHelp ) { // make strings inside * bold - $text = preg_replace("#\\*[^<>\n]+\\*#", '\\0', $text); + $text = preg_replace( "#\\*[^<>\n]+\\*#", '\\0', $text ); // make strings inside $ italic - $text = preg_replace("#\\$[^<>\n]+\\$#", '\\0', $text); + $text = preg_replace( "#\\$[^<>\n]+\\$#", '\\0', $text ); } /* Temporary fix for bad links in help messages. As a special case, @@ -248,7 +273,7 @@ See complete documentation, 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 complete documentation, 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/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php deleted file mode 100644 index 8cb3606d..00000000 --- a/includes/api/ApiFormatJson_json.php +++ /dev/null @@ -1,861 +0,0 @@ - -* @author Matt Knapp -* @author Brett Stimmerman -* @copyright 2005 Michal Migurski -* @version CVS: $Id: ApiFormatJson_json.php 45765 2009-01-15 10:18:44Z catrope $ -* @license http://www.opensource.org/licenses/bsd-license.php -* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 -*/ - -/** -* Marker constant for Services_JSON::decode(), used to flag stack state -*/ -define('SERVICES_JSON_SLICE', 1); - -/** -* Marker constant for Services_JSON::decode(), used to flag stack state -*/ -define('SERVICES_JSON_IN_STR', 2); - -/** -* Marker constant for Services_JSON::decode(), used to flag stack state -*/ -define('SERVICES_JSON_IN_ARR', 3); - -/** -* Marker constant for Services_JSON::decode(), used to flag stack state -*/ -define('SERVICES_JSON_IN_OBJ', 4); - -/** -* Marker constant for Services_JSON::decode(), used to flag stack state -*/ -define('SERVICES_JSON_IN_CMT', 5); - -/** -* Behavior switch for Services_JSON::decode() -*/ -define('SERVICES_JSON_LOOSE_TYPE', 16); - -/** -* Behavior switch for Services_JSON::decode() -*/ -define('SERVICES_JSON_SUPPRESS_ERRORS', 32); - -/** - * Converts to and from JSON format. - * - * Brief example of use: - * - * - * // create a new instance of Services_JSON - * $json = new Services_JSON(); - * - * // convert a complexe value to JSON notation, and send it to the browser - * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); - * $output = $json->encode($value); - * - * print($output); - * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] - * - * // accept incoming POST data, assumed to be in JSON notation - * $input = file_get_contents('php://input', 1000000); - * $value = $json->decode($input); - * - * - * @ingroup API - */ -class Services_JSON -{ - /** - * constructs a new JSON instance - * - * @param int $use object behavior flags; combine with boolean-OR - * - * possible values: - * - SERVICES_JSON_LOOSE_TYPE: loose typing. - * "{...}" syntax creates associative arrays - * instead of objects in decode(). - * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. - * Values which can't be encoded (e.g. resources) - * appear as NULL instead of throwing errors. - * By default, a deeply-nested resource will - * bubble up with an error, so all return values - * from encode() should be checked with isError() - */ - function Services_JSON($use = 0) - { - $this->use = $use; - } - - /** - * convert a string from one UTF-16 char to one UTF-8 char - * - * Normally should be handled by mb_convert_encoding, but - * provides a slower PHP-only method for installations - * that lack the multibye string extension. - * - * @param string $utf16 UTF-16 character - * @return string UTF-8 character - * @access private - */ - function utf162utf8($utf16) - { - // oh please oh please oh please oh please oh please - if(function_exists('mb_convert_encoding')) { - return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); - } - - $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); - - switch(true) { - case ((0x7F & $bytes) == $bytes): - // this case should never be reached, because we are in ASCII range - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0x7F & $bytes); - - case (0x07FF & $bytes) == $bytes: - // return a 2-byte UTF-8 character - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0xC0 | (($bytes >> 6) & 0x1F)) - . chr(0x80 | ($bytes & 0x3F)); - - case (0xFC00 & $bytes) == 0xD800 && strlen($utf16) >= 4 && (0xFC & ord($utf16{2})) == 0xDC: - // return a 4-byte UTF-8 character - $char = ((($bytes & 0x03FF) << 10) - | ((ord($utf16{2}) & 0x03) << 8) - | ord($utf16{3})); - $char += 0x10000; - return chr(0xF0 | (($char >> 18) & 0x07)) - . chr(0x80 | (($char >> 12) & 0x3F)) - . chr(0x80 | (($char >> 6) & 0x3F)) - . chr(0x80 | ($char & 0x3F)); - - case (0xFFFF & $bytes) == $bytes: - // return a 3-byte UTF-8 character - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0xE0 | (($bytes >> 12) & 0x0F)) - . chr(0x80 | (($bytes >> 6) & 0x3F)) - . chr(0x80 | ($bytes & 0x3F)); - } - - // ignoring UTF-32 for now, sorry - return ''; - } - - /** - * convert a string from one UTF-8 char to one UTF-16 char - * - * Normally should be handled by mb_convert_encoding, but - * provides a slower PHP-only method for installations - * that lack the multibye string extension. - * - * @param string $utf8 UTF-8 character - * @return string UTF-16 character - * @access private - */ - function utf82utf16($utf8) - { - // oh please oh please oh please oh please oh please - if(function_exists('mb_convert_encoding')) { - return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); - } - - switch(strlen($utf8)) { - case 1: - // this case should never be reached, because we are in ASCII range - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return $utf8; - - case 2: - // return a UTF-16 character from a 2-byte UTF-8 char - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr(0x07 & (ord($utf8{0}) >> 2)) - . chr((0xC0 & (ord($utf8{0}) << 6)) - | (0x3F & ord($utf8{1}))); - - case 3: - // return a UTF-16 character from a 3-byte UTF-8 char - // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - return chr((0xF0 & (ord($utf8{0}) << 4)) - | (0x0F & (ord($utf8{1}) >> 2))) - . chr((0xC0 & (ord($utf8{1}) << 6)) - | (0x7F & ord($utf8{2}))); - - case 4: - // return a UTF-16 surrogate pair from a 4-byte UTF-8 char - if(ord($utf8{0}) > 0xF4) return ''; # invalid - $char = ((0x1C0000 & (ord($utf8{0}) << 18)) - | (0x03F000 & (ord($utf8{1}) << 12)) - | (0x000FC0 & (ord($utf8{2}) << 6)) - | (0x00003F & ord($utf8{3}))); - if($char > 0x10FFFF) return ''; # invalid - $char -= 0x10000; - return chr(0xD8 | (($char >> 18) & 0x03)) - . chr(($char >> 10) & 0xFF) - . chr(0xDC | (($char >> 8) & 0x03)) - . chr($char & 0xFF); - } - - // ignoring UTF-32 for now, sorry - return ''; - } - - /** - * encodes an arbitrary variable into JSON format - * - * @param mixed $var any number, boolean, string, array, or object to be encoded. - * see argument 1 to Services_JSON() above for array-parsing behavior. - * if var is a strng, note that encode() always expects it - * to be in ASCII or UTF-8 format! - * @param bool $pretty pretty-print output with indents and newlines - * - * @return mixed JSON string representation of input var or an error if a problem occurs - * @access public - */ - function encode($var, $pretty=false) - { - $this->indent = 0; - $this->pretty = $pretty; - $this->nameValSeparator = $pretty ? ': ' : ':'; - return $this->encode2($var); - } - - /** - * encodes an arbitrary variable into JSON format - * - * @param mixed $var any number, boolean, string, array, or object to be encoded. - * see argument 1 to Services_JSON() above for array-parsing behavior. - * if var is a strng, note that encode() always expects it - * to be in ASCII or UTF-8 format! - * - * @return mixed JSON string representation of input var or an error if a problem occurs - * @access private - */ - function encode2($var) - { - if ($this->pretty) { - $close = "\n" . str_repeat("\t", $this->indent); - $open = $close . "\t"; - $mid = ',' . $open; - } - else { - $open = $close = ''; - $mid = ','; - } - - switch (gettype($var)) { - case 'boolean': - return $var ? 'true' : 'false'; - - case 'NULL': - return 'null'; - - case 'integer': - return (int) $var; - - case 'double': - case 'float': - return (float) $var; - - case 'string': - // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT - $ascii = ''; - $strlen_var = strlen($var); - - /* - * Iterate over every character in the string, - * escaping with a slash or encoding to UTF-8 where necessary - */ - for ($c = 0; $c < $strlen_var; ++$c) { - - $ord_var_c = ord($var{$c}); - - switch (true) { - case $ord_var_c == 0x08: - $ascii .= '\b'; - break; - case $ord_var_c == 0x09: - $ascii .= '\t'; - break; - case $ord_var_c == 0x0A: - $ascii .= '\n'; - break; - case $ord_var_c == 0x0C: - $ascii .= '\f'; - break; - case $ord_var_c == 0x0D: - $ascii .= '\r'; - break; - - case $ord_var_c == 0x22: - case $ord_var_c == 0x2F: - case $ord_var_c == 0x5C: - // double quote, slash, slosh - $ascii .= '\\'.$var{$c}; - break; - - case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): - // characters U-00000000 - U-0000007F (same as ASCII) - $ascii .= $var{$c}; - break; - - case (($ord_var_c & 0xE0) == 0xC0): - // characters U-00000080 - U-000007FF, mask 110XXXXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, ord($var{$c + 1})); - $c += 1; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xF0) == 0xE0): - // characters U-00000800 - U-0000FFFF, mask 1110XXXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2})); - $c += 2; - $utf16 = $this->utf82utf16($char); - $ascii .= sprintf('\u%04s', bin2hex($utf16)); - break; - - case (($ord_var_c & 0xF8) == 0xF0): - // characters U-00010000 - U-001FFFFF, mask 11110XXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - // These will always return a surrogate pair - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3})); - $c += 3; - $utf16 = $this->utf82utf16($char); - if($utf16 == '') { - $ascii .= '\ufffd'; - } else { - $utf16 = str_split($utf16, 2); - $ascii .= sprintf('\u%04s\u%04s', bin2hex($utf16[0]), bin2hex($utf16[1])); - } - break; - } - } - - return '"'.$ascii.'"'; - - case 'array': - /* - * As per JSON spec if any array key is not an integer - * we must treat the the whole array as an object. We - * also try to catch a sparsely populated associative - * array with numeric keys here because some JS engines - * will create an array with empty indexes up to - * max_index which can cause memory issues and because - * the keys, which may be relevant, will be remapped - * otherwise. - * - * As per the ECMA and JSON specification an object may - * have any string as a property. Unfortunately due to - * a hole in the ECMA specification if the key is a - * ECMA reserved word or starts with a digit the - * parameter is only accessible using ECMAScript's - * bracket notation. - */ - - // treat as a JSON object - if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { - $this->indent++; - $properties = array_map(array($this, 'name_value'), - array_keys($var), - array_values($var)); - $this->indent--; - - foreach($properties as $property) { - if(Services_JSON::isError($property)) { - return $property; - } - } - - return '{' . $open . join($mid, $properties) . $close . '}'; - } - - // treat it like a regular array - $this->indent++; - $elements = array_map(array($this, 'encode2'), $var); - $this->indent--; - - foreach($elements as $element) { - if(Services_JSON::isError($element)) { - return $element; - } - } - - return '[' . $open . join($mid, $elements) . $close . ']'; - - case 'object': - $vars = get_object_vars($var); - - $this->indent++; - $properties = array_map(array($this, 'name_value'), - array_keys($vars), - array_values($vars)); - $this->indent--; - - foreach($properties as $property) { - if(Services_JSON::isError($property)) { - return $property; - } - } - - return '{' . $open . join($mid, $properties) . $close . '}'; - - default: - return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) - ? 'null' - : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); - } - } - - /** - * array-walking function for use in generating JSON-formatted name-value pairs - * - * @param string $name name of key to use - * @param mixed $value reference to an array element to be encoded - * - * @return string JSON-formatted name-value pair, like '"name":value' - * @access private - */ - function name_value($name, $value) - { - $encoded_value = $this->encode2($value); - - if(Services_JSON::isError($encoded_value)) { - return $encoded_value; - } - - return $this->encode2(strval($name)) . $this->nameValSeparator . $encoded_value; - } - - /** - * reduce a string by removing leading and trailing comments and whitespace - * - * @param $str string string value to strip of comments and whitespace - * - * @return string string value stripped of comments and whitespace - * @access private - */ - function reduce_string($str) - { - $str = preg_replace(array( - - // eliminate single line comments in '// ...' form - '#^\s*//(.+)$#m', - - // eliminate multi-line comments in '/* ... */' form, at start of string - '#^\s*/\*(.+)\*/#Us', - - // eliminate multi-line comments in '/* ... */' form, at end of string - '#/\*(.+)\*/\s*$#Us' - - ), '', $str); - - // eliminate extraneous space - return trim($str); - } - - /** - * decodes a JSON string into appropriate variable - * - * @param string $str JSON-formatted string - * - * @return mixed number, boolean, string, array, or object - * corresponding to given JSON input string. - * See argument 1 to Services_JSON() above for object-output behavior. - * Note that decode() always returns strings - * in ASCII or UTF-8 format! - * @access public - */ - function decode($str) - { - $str = $this->reduce_string($str); - - switch (strtolower($str)) { - case 'true': - return true; - - case 'false': - return false; - - case 'null': - return null; - - default: - $m = array(); - - if (is_numeric($str)) { - // Lookie-loo, it's a number - - // This would work on its own, but I'm trying to be - // good about returning integers where appropriate: - // return (float)$str; - - // Return float or int, as appropriate - return ((float)$str == (integer)$str) - ? (integer)$str - : (float)$str; - - } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { - // STRINGS RETURNED IN UTF-8 FORMAT - $delim = substr($str, 0, 1); - $chrs = substr($str, 1, -1); - $utf8 = ''; - $strlen_chrs = strlen($chrs); - - for ($c = 0; $c < $strlen_chrs; ++$c) { - - $substr_chrs_c_2 = substr($chrs, $c, 2); - $ord_chrs_c = ord($chrs{$c}); - - switch (true) { - case $substr_chrs_c_2 == '\b': - $utf8 .= chr(0x08); - ++$c; - break; - case $substr_chrs_c_2 == '\t': - $utf8 .= chr(0x09); - ++$c; - break; - case $substr_chrs_c_2 == '\n': - $utf8 .= chr(0x0A); - ++$c; - break; - case $substr_chrs_c_2 == '\f': - $utf8 .= chr(0x0C); - ++$c; - break; - case $substr_chrs_c_2 == '\r': - $utf8 .= chr(0x0D); - ++$c; - break; - - case $substr_chrs_c_2 == '\\"': - case $substr_chrs_c_2 == '\\\'': - case $substr_chrs_c_2 == '\\\\': - case $substr_chrs_c_2 == '\\/': - if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || - ($delim == "'" && $substr_chrs_c_2 != '\\"')) { - $utf8 .= $chrs{++$c}; - } - break; - - case preg_match('/\\\uD[89AB][0-9A-F]{2}\\\uD[C-F][0-9A-F]{2}/i', substr($chrs, $c, 12)): - // escaped unicode surrogate pair - $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) - . chr(hexdec(substr($chrs, ($c + 4), 2))) - . chr(hexdec(substr($chrs, ($c + 8), 2))) - . chr(hexdec(substr($chrs, ($c + 10), 2))); - $utf8 .= $this->utf162utf8($utf16); - $c += 11; - break; - - case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): - // single, escaped unicode character - $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) - . chr(hexdec(substr($chrs, ($c + 4), 2))); - $utf8 .= $this->utf162utf8($utf16); - $c += 5; - break; - - case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): - $utf8 .= $chrs{$c}; - break; - - case ($ord_chrs_c & 0xE0) == 0xC0: - // characters U-00000080 - U-000007FF, mask 110XXXXX - //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 2); - ++$c; - break; - - case ($ord_chrs_c & 0xF0) == 0xE0: - // characters U-00000800 - U-0000FFFF, mask 1110XXXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 3); - $c += 2; - break; - - case ($ord_chrs_c & 0xF8) == 0xF0: - // characters U-00010000 - U-001FFFFF, mask 11110XXX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 4); - $c += 3; - break; - - case ($ord_chrs_c & 0xFC) == 0xF8: - // characters U-00200000 - U-03FFFFFF, mask 111110XX - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 5); - $c += 4; - break; - - case ($ord_chrs_c & 0xFE) == 0xFC: - // characters U-04000000 - U-7FFFFFFF, mask 1111110X - // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 - $utf8 .= substr($chrs, $c, 6); - $c += 5; - break; - - } - - } - - return $utf8; - - } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { - // array, or object notation - - if ($str{0} == '[') { - $stk = array(SERVICES_JSON_IN_ARR); - $arr = array(); - } else { - if ($this->use & SERVICES_JSON_LOOSE_TYPE) { - $stk = array(SERVICES_JSON_IN_OBJ); - $obj = array(); - } else { - $stk = array(SERVICES_JSON_IN_OBJ); - $obj = new stdClass(); - } - } - - array_push($stk, array( 'what' => SERVICES_JSON_SLICE, - 'where' => 0, - 'delim' => false)); - - $chrs = substr($str, 1, -1); - $chrs = $this->reduce_string($chrs); - - if ($chrs == '') { - if (reset($stk) == SERVICES_JSON_IN_ARR) { - return $arr; - - } else { - return $obj; - - } - } - - //print("\nparsing {$chrs}\n"); - - $strlen_chrs = strlen($chrs); - - for ($c = 0; $c <= $strlen_chrs; ++$c) { - - $top = end($stk); - $substr_chrs_c_2 = substr($chrs, $c, 2); - - if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { - // found a comma that is not inside a string, array, etc., - // OR we've reached the end of the character list - $slice = substr($chrs, $top['where'], ($c - $top['where'])); - array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); - //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - if (reset($stk) == SERVICES_JSON_IN_ARR) { - // we are in an array, so just push an element onto the stack - array_push($arr, $this->decode($slice)); - - } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { - // we are in an object, so figure - // out the property name and set an - // element in an associative array, - // for now - $parts = array(); - - if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { - // "name":value pair - $key = $this->decode($parts[1]); - $val = $this->decode($parts[2]); - - if ($this->use & SERVICES_JSON_LOOSE_TYPE) { - $obj[$key] = $val; - } else { - $obj->$key = $val; - } - } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { - // name:value pair, where name is unquoted - $key = $parts[1]; - $val = $this->decode($parts[2]); - - if ($this->use & SERVICES_JSON_LOOSE_TYPE) { - $obj[$key] = $val; - } else { - $obj->$key = $val; - } - } - - } - - } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { - // found a quote, and we are not inside a string - array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); - //print("Found start of string at {$c}\n"); - - } elseif (($chrs{$c} == $top['delim']) && - ($top['what'] == SERVICES_JSON_IN_STR) && - (($chrs{$c - 1} != '\\') || - ($chrs{$c - 1} == '\\' && $chrs{$c - 2} == '\\'))) { - // found a quote, we're in a string, and it's not escaped - array_pop($stk); - //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); - - } elseif (($chrs{$c} == '[') && - in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { - // found a left-bracket, and we are in an array, object, or slice - array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); - //print("Found start of array at {$c}\n"); - - } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { - // found a right-bracket, and we're in an array - array_pop($stk); - //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - } elseif (($chrs{$c} == '{') && - in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { - // found a left-brace, and we are in an array, object, or slice - array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); - //print("Found start of object at {$c}\n"); - - } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { - // found a right-brace, and we're in an object - array_pop($stk); - //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - } elseif (($substr_chrs_c_2 == '/*') && - in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { - // found a comment start, and we are in an array, object, or slice - array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); - $c++; - //print("Found start of comment at {$c}\n"); - - } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { - // found a comment end, and we're in one now - array_pop($stk); - $c++; - - for ($i = $top['where']; $i <= $c; ++$i) - $chrs = substr_replace($chrs, ' ', $i, 1); - - //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); - - } - - } - - if (reset($stk) == SERVICES_JSON_IN_ARR) { - return $arr; - - } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { - return $obj; - - } - - } - } - } - - /** - * @todo Ultimately, this should just call PEAR::isError() - */ - function isError($data, $code = null) - { - if (class_exists('pear')) { - return PEAR::isError($data, $code); - } elseif (is_object($data) && (get_class($data) == 'services_json_error' || - is_subclass_of($data, 'services_json_error'))) { - return true; - } - - return false; - } -} - - -// Hide the PEAR_Error variant from Doxygen -/// @cond -if (class_exists('PEAR_Error')) { - - /** - * @ingroup API - */ - class Services_JSON_Error extends PEAR_Error - { - function Services_JSON_Error($message = 'unknown error', $code = null, - $mode = null, $options = null, $userinfo = null) - { - parent::PEAR_Error($message, $code, $mode, $options, $userinfo); - } - } - -} else { -/// @endcond - - /** - * @todo Ultimately, this class shall be descended from PEAR_Error - * @ingroup API - */ - class Services_JSON_Error - { - function Services_JSON_Error($message = 'unknown error', $code = null, - $mode = null, $options = null, $userinfo = null) - { - - } - } -} 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 = "
    \xc2\xa0"; - 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("$nl"); - $this->printText("$nl"); - $this->printText("$indstr
    $nl"); - $this->printText("$indstr$nl"); - $this->slowWddxPrinter($this->getResultData(), 4); - $this->printText("$indstr$nl"); - $this->printText("$nl"); + $this->printText( "$nl" ); + $this->printText( "$nl" ); + $this->printText( "$indstr
    $nl" ); + $this->printText( "$indstr$nl" ); + $this->slowWddxPrinter( $this->getResultData(), 4 ); + $this->printText( "$indstr$nl" ); + $this->printText( "$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 () // or a regular 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$nl"); + $this->printText( $indstr . Xml::element( 'array', array( + 'length' => $cnt ), null ) . $nl ); + foreach ( $elemValue as $subElemValue ) + $this->slowWddxPrinter( $subElemValue, $indent + 2 ); + $this->printText( "$indstr$nl" ); } else { // Associative array () - $this->printText("$indstr$nl"); - foreach($elemValue as $subElemName => $subElemValue) { - $this->printText($indstr2 . Xml::element('var', array( + $this->printText( "$indstr$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$nl"); + ), null ) . $nl ); + $this->slowWddxPrinter( $subElemValue, $indent + 4 ); + $this->printText( "$indstr2$nl" ); } - $this->printText("$indstr$nl"); + $this->printText( "$indstr$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(''); - $this->recXmlPrint($this->mRootElemName, $this->getResultData(), $this->getIsHtml() ? -2 : null); + $this->mXslt = $params['xslt']; + + $this->printText( '' ); + 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( '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 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 @@ @gmail.com, + * Copyright © 2006-2007 Yuri Astrakhan @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 .@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 @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,49 +36,70 @@ 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(); return $cachedPatrolToken; } + /** + * 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. */ @@ -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 @@ +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 text 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 @@ + + * + * 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 @@ +.@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 @@ -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
    \n";
    -
    -			$context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
    -			$text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n
    \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 @@ - '{[}', '}' => '{]}' ) ); -} - -/** - * 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
    \n";
    -
    -			$context = rtrim( str_replace( "\t", " ", substr( $this->mText, $startLine, $endLine - $startLine ) ) );
    -			$text .= htmlspecialchars( $context ) . "\n" . str_repeat( ' ', $pos - $startLine ) . "^\n
    \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 {

    {title}

    }} - -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 { @@ -191,6 +192,14 @@ class Database { return true; } + /** + * 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; } @@ -1439,12 +1273,33 @@ class Database { return $list; } + /** + * 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 ) ) { @@ -2047,6 +1995,21 @@ class Database { $this->commit(); } + /** + * 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 */ @@ -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,13 +2165,44 @@ 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) @@ -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 = "

    $sorry
    $again

    $info

    "; $text = str_replace( '$1', $this->error, $noconnect ); - /* - if ( $GLOBALS['wgShowExceptionDetails'] ) { - $text .= '

    Backtrace:

    ' . - nl2br( htmlspecialchars( $this->getTraceAsString() ) ) . - "

    \n"; - }*/ + if ( $wgShowDBErrorBacktrace ) { + $text .= '

    Backtrace:

    ' . 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('',''), '', $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('',''), '', $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 = "

    $cachederror
    "; + # Output cached page with notices on bottom and re-close body + return "{$cache}{$warning}
    $text
    $extra"; } - $warning = "
    $cachederror
    "; - # Output cached page with notices on bottom and re-close body - return "{$cache}{$warning}
    $text
    $extra"; + } 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 { - -
    @@ -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 .= '

    Backtrace:

    ' . 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 @@ -8,38 +8,34 @@ * @author leo.petr+mediawiki@gmail.com */ -/** - * 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 = <<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 "

  • $string
  • "; + 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 "
    ";
     			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 " FAILED: " . htmlspecialchars( $res ) . "";
    +			} else {
    +				print " done";
    +			}
     			$res = null;
     	
     			// TODO: update mediawiki_version table
     			
     			// TODO: populate interwiki links
     			
    -			$this->commit();
    +			if ($this->lastError()) {
    +				print "
  • Errors encountered during table creation -- rolled back
  • \n"; + print "
  • Please install again
  • \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) ) { @@ -744,42 +796,10 @@ 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' ); } @@ -930,89 +949,19 @@ EOF; return $list; } - /** - * 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; } /** @@ -1068,140 +1024,181 @@ EOF; return $this->mInsertId; } + /** + * 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 ) ) { @@ -1237,21 +1234,12 @@ EOF; return db2_num_rows( $this->mLastResult ); } - /** - * 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 = ''; @@ -1462,47 +1449,19 @@ EOF; return "[http://www.ibm.com/software/data/db2/express/?s_cmp=ECDDWW01&s_tact=MediaWiki IBM DB2]"; } - /** - * 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; } @@ -445,22 +449,6 @@ class DatabaseMssql extends Database { return $this->query( $sql, $fname ); } - /** - * Estimate rows in dataset - * Returns estimated count, based on EXPLAIN output - * Takes same arguments as Database::select() - */ - function estimateRowCount( $table, $vars='*', $conds='', $fname = 'Database::estimateRowCount', $options = array() ) { - $rows = 0; - $res = $this->select ($table, 'COUNT(*)', $conds, $fname, $options ); - if ($res) { - $row = $this->fetchObject($res); - $rows = $row[0]; - } - $this->freeResult($res); - return $rows; - } - /** * Determines whether a field exists in a table * Usually aborts on failure @@ -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(); @@ -707,13 +695,6 @@ class DatabaseMssql extends Database { return str_replace("'","''",$s); } - /** - * USE INDEX clause - */ - function useIndexClause( $index ) { - return ""; - } - /** * REPLACE query wrapper * PostgreSQL simulates this with a DELETE followed by INSERT @@ -857,18 +838,6 @@ class DatabaseMssql extends Database { return $sql; } - /** - * 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 @@ -877,22 +846,6 @@ class DatabaseMssql extends Database { return $this->lastErrno() == 1205; } - /** - * Begin a transaction, committing any previously open transaction - * @deprecated use begin() - */ - function immediateBegin( $fname = 'Database::immediateBegin' ) { - $this->begin(); - } - - /** - * Commit transaction, if one is open - * @deprecated use commit() - */ - function immediateCommit( $fname = 'Database::immediateCommit' ) { - $this->commit(); - } - /** * Return MW-style timestamp used for MySQL schema */ @@ -930,16 +883,6 @@ class DatabaseMssql extends Database { return $sql; } - /** - * not done - */ - public function setTimeout($timeout) { return; } - - function ping() { - wfDebug("Function ping() not written for MSSQL yet"); - return true; - } - /** * How lagged is this slave? */ @@ -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 @@ +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; } @@ -90,31 +112,88 @@ 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.\n"; + } else { + print " FAILED\n"; + dieout( htmlspecialchars( $res ) ); + } + + // Avoid the non-standard "REPLACE INTO" syntax + echo "
  • Populating interwiki table
  • \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 "
  • Table interwiki successfully populated
  • \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 = <<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 = <<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 "FAILED. Make sure that the user \"" . htmlspecialchars( $wgDBuser ) . "\" can write to the schema \"" . htmlspecialchars( $wgDBmwschema ) . "\"\n"; - dieout(""); + dieout(""); # Will close the main list
      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.\n"; + } else { + print " FAILED\n"; + dieout( htmlspecialchars( $res ) ); + } ## Update version information $mwv = $this->addQuotes($wgVersion); @@ -1274,10 +1339,13 @@ END; "WHERE type = 'Creation'"; $this->query($SQL); + echo "
    • Populating interwiki table... "; + ## Avoid the non-standard "REPLACE INTO" syntax $f = fopen( "../maintenance/interwiki.sql", 'r' ); if ($f == false ) { - dieout( "
    • Could not find the interwiki.sql file"); + print "FAILED
    • "; + 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.\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 " FAILED"; + dieout( htmlspecialchars( $err ) ); } + echo " done."; # Use DatabasePostgres's code to populate interwiki from MySQL template - $f = fopen("$IP/maintenance/interwiki.sql",'r'); - if ($f == false) dieout("
    • 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,8 +565,30 @@ 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 */ @@ -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=<<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', "$t", $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 = '   ' . $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 = ' [' . $sk->makeKnownLinkObj( $this->mTitle, - wfMsgHtml( 'markaspatrolleddiff' ), "action=markpatrolled&rcid={$rcid}" ) . ']'; - } 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 = ' '; - } 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 = "   $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 = "   $rdel "; - } - - $oldHeader = '
      '.$this->mOldtitle.'
      ' . - '
      ' . $sk->revUserTools( $this->mOldRev, !$this->unhide ) . "
      " . - '
      ' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel."
      " . - '
      ' . $prevlink .'
      '; - $newHeader = '
      '.$this->mNewtitle.'
      ' . - '
      ' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . " $rollback
      " . - '
      ' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel."
      " . - '
      ' . $nextlink . $patrol . '
      '; - - # 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( "\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( "\n", - array( 'rev-deleted-unhide-diff', $link ) ); - } - } else if( $wgEnableHtmlDiff && $this->htmldiff ) { - $multi = $this->getMultiNotice(); - $wgOut->addHTML('
      '.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ), - 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'
      '); - $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) ); - $this->renderHtmlDiff(); - } else { - if( $wgEnableHtmlDiff ) { - $wgOut->addHTML('
      '.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ), - 'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'
      '); - } - $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( "

      {$this->mPagetitle}

      \n" ); - # Add deleted rev tag if needed - if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); - } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\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
       and don't parse
      -				$m = array();
      -				preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
      -				$wgOut->addHTML( "
      \n" );
      -				$wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
      -				$wgOut->addHTML( "\n
      \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( - "' - ); - } - - wfProfileOut( __METHOD__ ); - } - - - function renderHtmlDiff() { - global $wgOut, $wgTitle, $wgParser, $wgDebugComments; - wfProfileIn( __METHOD__ ); - - $this->showDiffStyle(); - - $wgOut->addHTML( '

      '.wfMsgHtml( 'visual-comparison' )."

      \n" ); - #add deleted rev tag if needed - if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); - } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $wgOut->wrapWikiMsg( "\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" ); - } - - 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', "$t", $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 = '
      ' . $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), - 'diff=next&oldid=' . $this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' ); - } - $header = "
      " . - $sk->revUserTools( $this->mNewRev ) . "
      " . $sk->revComment( $this->mNewRev ) . $nextlink . "
      \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', "(fixme, bug)", '' ); - 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( "" ); - } - - /** - * 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\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 "\n"; - } - - /** - * Replace line numbers with the text in the user's language - */ - function localiseLineNumbers( $text ) { - return preg_replace_callback( '//', - 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 = " - - - - - - - - - - "; - - if ( $multi != '' ) - $header .= ""; - - return $header . $diff . "
      {$otitle}{$ntitle}
      {$multi}
      "; - } - - /** - * 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 = "{$this->mPagetitle}"; - $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; - - } else { - $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid ); - $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid ); - $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp ); - - $this->mNewtitle = "{$this->mPagetitle}"; - $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; - } - if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { - $this->mNewtitle = "{$this->mPagetitle}"; - } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { - $this->mNewtitle = ''.$this->mNewtitle.''; - } - - // 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 = "{$this->mOldPagetitle}" - . " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; - // 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 .= " (" . $htmlLink . ")"; - } - - if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) { - $this->mOldtitle = '' . $this->mOldPagetitle . ''; - } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { - $this->mOldtitle = '' . $this->mOldtitle . ''; - } - } - - 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 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 @@ +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=<<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', "$t", $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 = '   ' . $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 = ' [' . $sk->link( + $this->mTitle, + wfMsgHtml( 'markaspatrolleddiff' ), + array(), + array( + 'action' => 'markpatrolled', + 'rcid' => $rcid + ), + array( + 'known', + 'noclasses' + ) + ) . ']'; + } 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 = ' '; + } 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 = '
      '.$this->mOldtitle.'
      ' . + '
      ' . + $sk->revUserTools( $this->mOldRev, !$this->unhide ).'
      ' . + '
      ' . $oldminor . + $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel.'
      ' . + '
      ' . $prevlink .'
      '; + $newHeader = '
      '.$this->mNewtitle.'
      ' . + '
      ' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) . + " $rollback
      " . + '
      ' . $newminor . + $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel.'
      ' . + '
      ' . $nextlink . $patrol . '
      '; + + # 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( "\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( "\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 = "\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 = '   ' . $link . ' '; + } + return $link; + } + + /** + * Show the new revision of the page. + */ + function renderNewRevision() { + global $wgOut, $wgUser; + wfProfileIn( __METHOD__ ); + + $wgOut->addHTML( "

      {$this->mPagetitle}

      \n" ); + # Add deleted rev tag if needed + if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "\n", 'rev-deleted-text-permission' ); + } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "\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
       and don't parse
      +				$m = array();
      +				preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
      +				$wgOut->addHTML( "
      \n" );
      +				$wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
      +				$wgOut->addHTML( "\n
      \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( + "' + ); + } + + 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', "$t", $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 = '
      ' . $sk->link( + $this->mTitle, + wfMsgHtml( 'nextdiff' ), + array( + 'id' => 'differences-nextlink' + ), + array( + 'diff' => 'next', + 'oldid' => $this->mNewid, + ), + array( + 'known', + 'noclasses' + ) + ); + } + $header = "
      " . + $sk->revUserTools( $this->mNewRev ) . "
      " . $sk->revComment( $this->mNewRev ) . $nextlink . "
      \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', "(fixme, bug)", '' ); + 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( "" ); + } + + /** + * 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\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 "\n"; + } + + /** + * Replace line numbers with the text in the user's language + */ + function localiseLineNumbers( $text ) { + return preg_replace_callback( '//', + 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 = ""; + if( $diff ) { // Safari/Chrome show broken output if cols not used + $header .= " + + + + "; + $colspan = 2; + $multiColspan = 4; + } else { + $colspan = 1; + $multiColspan = 2; + } + $header .= " + + + + "; + + if ( $multi != '' ) { + $header .= ""; + } + if ( $notice != '' ) { + $header .= ""; + } + + return $header . $diff . "
      {$otitle}{$ntitle}
      {$multi}
      {$notice}
      "; + } + + /** + * 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 = "{$this->mPagetitle}"; + $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; + } 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 = "{$this->mPagetitle}"; + $this->mNewtitle .= " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; + } + if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) { + $this->mNewtitle = "{$this->mPagetitle}"; + } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) { + $this->mNewtitle = "{$this->mNewtitle}"; + } + + // 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 = "{$this->mOldPagetitle}" + . " (" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . ")"; + // 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 .= " (" . $htmlLink . ")"; + } + + if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) { + $this->mOldtitle = '' . $this->mOldPagetitle . ''; + } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) { + $this->mOldtitle = '' . $this->mOldtitle . ''; + } + } + + 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 @@ - - * - * 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, ''.Sanitizer::hackDocType().'', false) - || !xml_parse($xml_parser, $from, false) - || !xml_parse($xml_parser, '', 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, ''.Sanitizer::hackDocType().'', false) - || !xml_parse($xml_parser, $to, false) - || !xml_parse($xml_parser, '', 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('
        '); - $rootlistopened = true; - } - $nbDifferences = count($differences); - for ($j = 0; $j < $nbDifferences; ++$j) { - $d = $differences[$j]; - $lvl1listopened = false; - if ($rootlistopened) { - $txt->addHtml('
      • '); - } - if ($d->leftlength + $d->rightlength > 1) { - $txt->addHtml('
          '); - $lvl1listopened = true; - } - // left are the old ones - for ($i = $d->leftstart; $i < $d->leftend; ++$i) { - if ($lvl1listopened){ - $txt->addHtml('
        • '); - } - // add a bullet for a old tag - $this->addTagOld($txt, $this->other->ancestors[$i]); - if ($lvl1listopened){ - $txt->addHtml('
        • '); - } - } - // right are the new ones - for ($i = $d->rightstart; $i < $d->rightend; ++$i) { - if ($lvl1listopened){ - $txt->addHtml('
        • '); - } - // add a bullet for a new tag - $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]); - - if ($lvl1listopened){ - $txt->addHtml('
        • '); - } - } - if ($lvl1listopened) { - $txt->addHtml('
        '); - } - if ($rootlistopened) { - $txt->addHtml('
      • '); - } - } - if ($rootlistopened) { - $txt->addHtml('
      '); - } - 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 = "<" . $this->node->qName . ">"; - } - 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 = "<" . $this->node->qName . ">"; - } - 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 = "<" . $name . ">";; - } - 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 = "<" . $this->node->qName . ">"; - } - $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 = "<" . $this->node->qName . ">"; - } - $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 @@ - - * - * 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, ''); - }else{ - parent::__construct($parent, '' . strtolower($attrs['src']) . ''); - } - $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 @@ + 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 @@ + '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 @@ +, 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 */ @@ -202,6 +208,13 @@ class ArchivedFile return $this->key; } + /** + * Return the FileStore key (overriding base File class) + */ + public function getStorageKey() { + return $this->getKey(); + } + /** * Return the FileStore storage group */ @@ -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) { @@ -1124,6 +1139,19 @@ abstract class File { return self::sha1Base36( $this->getPath() ); } + /** + * Get the deletion archive key, . + */ + 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. @@ -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 @@ -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; } } @@ -202,6 +235,15 @@ abstract class FileRepo { return $this->thumbScriptUrl; } + /** + * 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 */ @@ -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(); } @@ -1702,6 +1733,38 @@ class LocalFileRestoreBatch { return $status; } + /** + * 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 @@ +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/json/Services_JSON.php b/includes/json/Services_JSON.php new file mode 100644 index 00000000..94233520 --- /dev/null +++ b/includes/json/Services_JSON.php @@ -0,0 +1,875 @@ + +* @author Matt Knapp +* @author Brett Stimmerman +* @copyright 2005 Michal Migurski +* @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 +*/ + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_SLICE', 1); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_STR', 2); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_ARR', 3); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_OBJ', 4); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_CMT', 5); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** + * Converts to and from JSON format. + * + * Brief example of use: + * + * + * // create a new instance of Services_JSON + * $json = new Services_JSON(); + * + * // convert a complexe value to JSON notation, and send it to the browser + * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + * $output = $json->encode($value); + * + * print($output); + * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + * + * // accept incoming POST data, assumed to be in JSON notation + * $input = file_get_contents('php://input', 1000000); + * $value = $json->decode($input); + * + * + * @ingroup API + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $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 + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFC00 & $bytes) == 0xD800 && strlen($utf16) >= 4 && (0xFC & ord($utf16{2})) == 0xDC: + // return a 4-byte UTF-8 character + $char = ((($bytes & 0x03FF) << 10) + | ((ord($utf16{2}) & 0x03) << 8) + | ord($utf16{3})); + $char += 0x10000; + return chr(0xF0 | (($char >> 18) & 0x07)) + . chr(0x80 | (($char >> 12) & 0x3F)) + . chr(0x80 | (($char >> 6) & 0x3F)) + . chr(0x80 | ($char & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + + case 4: + // return a UTF-16 surrogate pair from a 4-byte UTF-8 char + if(ord($utf8{0}) > 0xF4) return ''; # invalid + $char = ((0x1C0000 & (ord($utf8{0}) << 18)) + | (0x03F000 & (ord($utf8{1}) << 12)) + | (0x000FC0 & (ord($utf8{2}) << 6)) + | (0x00003F & ord($utf8{3}))); + if($char > 0x10FFFF) return ''; # invalid + $char -= 0x10000; + return chr(0xD8 | (($char >> 18) & 0x03)) + . chr(($char >> 10) & 0xFF) + . chr(0xDC | (($char >> 8) & 0x03)) + . chr($char & 0xFF); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * @param bool $pretty pretty-print output with indents and newlines + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var, $pretty=false) + { + $this->indent = 0; + $this->pretty = $pretty; + $this->nameValSeparator = $pretty ? ': ' : ':'; + return $this->encode2($var); + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access private + */ + function encode2($var) + { + if ($this->pretty) { + $close = "\n" . str_repeat("\t", $this->indent); + $open = $close . "\t"; + $mid = ',' . $open; + } + else { + $open = $close = ''; + $mid = ','; + } + + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + // These will always return a surrogate pair + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + if($utf16 == '') { + $ascii .= '\ufffd'; + } else { + $utf16 = str_split($utf16, 2); + $ascii .= sprintf('\u%04s\u%04s', bin2hex($utf16[0]), bin2hex($utf16[1])); + } + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $this->indent++; + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + $this->indent--; + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . $open . join($mid, $properties) . $close . '}'; + } + + // treat it like a regular array + $this->indent++; + $elements = array_map(array($this, 'encode2'), $var); + $this->indent--; + + foreach($elements as $element) { + if(Services_JSON::isError($element)) { + return $element; + } + } + + return '[' . $open . join($mid, $elements) . $close . ']'; + + case 'object': + $vars = get_object_vars($var); + + $this->indent++; + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + $this->indent--; + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . $open . join($mid, $properties) . $close . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode2($value); + + if(Services_JSON::isError($encoded_value)) { + return $encoded_value; + } + + return $this->encode2(strval($name)) . $this->nameValSeparator . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\uD[89AB][0-9A-F]{2}\\\uD[C-F][0-9A-F]{2}/i', substr($chrs, $c, 12)): + // escaped unicode surrogate pair + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))) + . chr(hexdec(substr($chrs, ($c + 8), 2))) + . chr(hexdec(substr($chrs, ($c + 10), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 11; + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array( 'what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + (($chrs{$c - 1} != '\\') || + ($chrs{$c - 1} == '\\' && $chrs{$c - 2} == '\\'))) { + // found a quote, we're in a string, and it's not escaped + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + 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; + } + + return false; + } +} + + +// Hide the PEAR_Error variant from Doxygen +/// @cond +if (class_exists('PEAR_Error')) { + + /** + * @ingroup API + */ + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { +/// @endcond + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + * @ingroup API + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } +} 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 @@ +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 @@ + $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 ); @@ -179,6 +179,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: * @@ -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 * @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 @@ +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( '', '', '$1', $text, 'i' ); + + $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); + return Xml::openElement( 'pre', $attribs ) . + Xml::escapeTagsOnly( $content ) . + '
      '; + } + + static function html( $content, $attributes, $parser ) { + global $wgRawHtml; + if( $wgRawHtml ) { + return array( $content, 'markerType' => 'nowiki' ); + } else { + throw new MWException( ' extension tag encountered unexpectedly' ); + } + } + + static function nowiki( $content, $attributes, $parser ) { + $content = strtr( $content, array( '-{' => '-{', '}-' => '}-' ) ); + 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 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 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 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 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"; } $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( '
      ' , $indent_level ) . ""; - 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
      ', $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 - () | # m[1]: Skip link text + () | # 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 "{$keyword} {$id}"; } elseif ( isset( $m[5] ) && $m[5] !== '' ) { # ISBN @@ -971,7 +1032,7 @@ class Parser $titleObj = SpecialPage::getTitleFor( 'Booksources', $num ); return'ISBN $isbn"; + "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn"; } 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 = 'mLastSection . ">\n"; } $this->mInPre = false; @@ -1933,9 +1936,9 @@ class Parser $result = $this->closeParagraph(); if ( '*' === $char ) { $result .= '
      • '; } - else if ( '#' === $char ) { $result .= '
        1. '; } - else if ( ':' === $char ) { $result .= '
          '; } - else if ( ';' === $char ) { + elseif ( '#' === $char ) { $result .= '
          1. '; } + elseif ( ':' === $char ) { $result .= '
            '; } + elseif ( ';' === $char ) { $result .= '
            '; $this->mDTopen = true; } @@ -1946,7 +1949,7 @@ class Parser /* private */ function nextItem( $char ) { if ( '*' === $char || '#' === $char ) { return '
          2. '; } - else if ( ':' === $char || ';' === $char ) { + elseif ( ':' === $char || ';' === $char ) { $close = '
          '; if ( $this->mDTopen ) { $close = ''; } if ( ';' === $char ) { @@ -1962,8 +1965,8 @@ class Parser /* private */ function closeList( $char ) { if ( '*' === $char ) { $text = '
      '; } - else if ( '#' === $char ) { $text = '
    • '; } - else if ( ':' === $char ) { + elseif ( '#' === $char ) { $text = ''; } + elseif ( ':' === $char ) { if ( $this->mDTopen ) { $this->mDTopen = false; $text = ''; @@ -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('/
       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.'
      '; $paragraphStack = false; @@ -2138,7 +2159,7 @@ class Parser $output .= $this->closeList( $prefix2[$prefixLength-1] ); --$prefixLength; } - if ( '' != $this->mLastSection ) { + if ( $this->mLastSection != '' ) { $output .= '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( ' extension tag encountered unexpectedly' ); - } - case 'nowiki': - $content = strtr($content, array('-{' => '-{', '}-' => '}-')); - $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 = 'Invalid tag extension name: ' . - htmlspecialchars( $name ) . ''; - } + + 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 = 'Invalid tag extension name: ' . + htmlspecialchars( $name ) . ''; + } + + 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,27 +3442,47 @@ 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 @@ -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 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 @@ -3683,6 +3835,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. some stuff. + * 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 placeholders with actual links, in the buffer * Placeholders created in Skin::makeLinkObj() * Returns an array of link CSS classes, indexed by PDBK. @@ -4084,19 +4358,6 @@ class Parser return $this->mLinkHolders->replaceText( $text ); } - /** - * Tag hook handler for 'pre'. - */ - function renderPreTag( $text, $attribs ) { - // Backwards-compatibility hack - $content = StringUtils::delimiterReplace( '', '', '$1', $text, 'i' ); - - $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' ); - return Xml::openElement( 'pre', $attribs ) . - Xml::escapeTagsOnly( $content ) . - '
      '; - } - /** * Renders an image gallery from a text with one line per image. * text labels may be given by using |-style alternative text. E.g. @@ -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( "" ); } - + $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 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 @@ -65,6 +65,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 */ 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/search/SearchEngine.php b/includes/search/SearchEngine.php new file mode 100644 index 00000000..f4ca700d --- /dev/null +++ b/includes/search/SearchEngine.php @@ -0,0 +1,1248 @@ + test prefix:Main Page/Archive + */ + function transformSearchTerm( $term ) { + return $term; + } + + /** + * If an exact title match can be found, or a very slightly close match, + * return the title. If no match, returns NULL. + * + * @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() ) { + $allSearchTerms = array_merge($allSearchTerms,$wgContLang->convertLinkToAllVariants($searchterm)); + } + + 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; + + if ( $title->getNamespace() == NS_SPECIAL || $title->isExternal() || $title->exists() ) { + return $title; + } + + # See if it still otherwise has content is some sane sense + $article = MediaWiki::articleFromTitle( $title ); + if( $article->hasViewableContent() ) { + return $title; + } + + # Now try all lower case (i.e. first letter capitalized) + # + $title = Title::newFromText( $wgContLang->lc( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + # Now try capitalized string + # + $title = Title::newFromText( $wgContLang->ucwords( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + # Now try all upper case + # + $title = Title::newFromText( $wgContLang->uc( $term ) ); + if ( $title && $title->exists() ) { + return $title; + } + + # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc + $title = Title::newFromText( $wgContLang->ucwordbreaks($term) ); + if ( $title && $title->exists() ) { + return $title; + } + + // Give hooks a chance at better match variants + $title = null; + if( !wfRunHooks( 'SearchGetNearMatch', array( $term, &$title ) ) ) { + return $title; + } + } + + $title = Title::newFromText( $searchterm ); + + # Entering an IP address goes to the contributions page + if ( ( $title->getNamespace() == NS_USER && User::isIP($title->getText() ) ) + || User::isIP( trim( $searchterm ) ) ) { + return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ); + } + + + # Entering a user goes to the user page whether it's there or not + if ( $title->getNamespace() == NS_USER ) { + return $title; + } + + # Go to images that exist even if there's no local page. + # There may have been a funny upload, or it may be on a shared + # file repository such as Wikimedia Commons. + if( $title->getNamespace() == NS_FILE ) { + $image = wfFindFile( $title ); + if( $image ) { + return $title; + } + } + + # MediaWiki namespace? Page may be "implied" if not customized. + # Just return it, with caps forced as the message system likes it. + if( $title->getNamespace() == NS_MEDIAWIKI ) { + return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( $title->getText() ) ); + } + + # Quoted term? Try without the quotes... + $matches = array(); + if( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { + return SearchEngine::getNearMatch( $matches[1] ); + } + + return null; + } + + public static function legalSearchChars() { + return "A-Za-z_'.0-9\\x80-\\xFF\\-"; + } + + /** + * Set the maximum number of results to return + * and how many to skip before returning the first. + * + * @param $limit Integer + * @param $offset Integer + */ + function setLimitOffset( $limit, $offset = 0 ) { + $this->limit = intval( $limit ); + $this->offset = intval( $offset ); + } + + /** + * Set which namespaces the search should include. + * Give an array of namespace index numbers. + * + * @param $namespaces Array + */ + function setNamespaces( $namespaces ) { + $this->namespaces = $namespaces; + } + + /** + * Parse some common prefixes: all (search everything) + * or namespace names + * + * @param $query String + */ + function replacePrefixes( $query ){ + global $wgContLang; + + $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; + $parsed = substr($query,strlen($allkeyword)); + } else if( strpos($query,':') !== false ) { + $prefix = substr($query,0,strpos($query,':')); + $index = $wgContLang->getNsIndex($prefix); + if($index !== false){ + $this->namespaces = array($index); + $parsed = substr($query,strlen($prefix)+1); + } + } + if(trim($parsed) == '') + $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 + */ + public static function searchableNamespaces() { + global $wgContLang; + $arr = array(); + foreach( $wgContLang->getNamespaces() as $ns => $name ) { + if( $ns >= NS_MAIN ) { + $arr[$ns] = $name; + } + } + + wfRunHooks( 'SearchableNamespaces', array( &$arr ) ); + return $arr; + } + + /** + * Extract default namespaces to search from the given user's + * settings, returning a list of index numbers. + * + * @param $user User + * @return Array + */ + 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 + */ + public static function userHighlightPrefs( &$user ){ + //$contextlines = $user->getOption( 'contextlines', 5 ); + //$contextchars = $user->getOption( 'contextchars', 50 ); + $contextlines = 2; // Hardcode this. Old defaults sucked. :) + $contextchars = 75; // same as above.... :P + return array($contextlines, $contextchars); + } + + /** + * An array of namespaces indexes to be searched by default + * + * @return Array + */ + public static function defaultNamespaces(){ + global $wgNamespacesToBeSearchedDefault; + + return array_keys($wgNamespacesToBeSearchedDefault, true); + } + + /** + * Get a list of namespace names useful for showing in tooltips + * and preferences + * + * @param $namespaces Array + */ + public static function namespacesAsText( $namespaces ){ + global $wgContLang; + + $formatted = array_map( array($wgContLang,'getFormattedNsText'), $namespaces ); + foreach( $formatted as $key => $ns ){ + if ( empty($ns) ) + $formatted[$key] = wfMsg( 'blanknamespace' ); + } + return $formatted; + } + + /** + * Return the help namespaces to be shown on Special:Search + * + * @return Array + */ + public static function helpNamespaces() { + global $wgNamespacesToBeSearchedHelp; + + return array_keys( $wgNamespacesToBeSearchedHelp, true ); + } + + /** + * Return a 'cleaned up' search string + * + * @param $text String + * @return String + */ + function filter( $text ) { + $lc = $this->legalSearchChars(); + return trim( preg_replace( "/[^{$lc}]/", " ", $text ) ); + } + /** + * Load up the appropriate search engine class for the currently + * active database backend, and return a configured instance. + * + * @return SearchEngine + */ + public static function create() { + global $wgSearchType; + $dbr = wfGetDB( DB_SLAVE ); + if( $wgSearchType ) { + $class = $wgSearchType; + } else { + $class = $dbr->getSearchEngine(); + } + $search = new $class( $dbr ); + $search->setLimitOffset(0,0); + return $search; + } + + /** + * Create or update the search index record for the given page. + * Title and text should be pre-processed. + * STUB + * + * @param $id Integer + * @param $title String + * @param $text String + */ + function update( $id, $title, $text ) { + // no-op + } + + /** + * Update a search index record's title only. + * Title should be pre-processed. + * STUB + * + * @param $id Integer + * @param $title String + */ + function updateTitle( $id, $title ) { + // no-op + } + + /** + * Get OpenSearch suggestion template + * + * @return String + */ + public static function getOpenSearchTemplate() { + global $wgOpenSearchTemplate, $wgServer, $wgScriptPath; + if( $wgOpenSearchTemplate ) { + return $wgOpenSearchTemplate; + } else { + $ns = implode( '|', SearchEngine::defaultNamespaces() ); + if( !$ns ) $ns = "0"; + return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace='.$ns; + } + } + + /** + * Get internal MediaWiki Suggest template + * + * @return String + */ + public static function getMWSuggestTemplate() { + global $wgMWSuggestTemplate, $wgServer, $wgScriptPath; + if($wgMWSuggestTemplate) + return $wgMWSuggestTemplate; + else + return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}&suggest'; + } +} + +/** + * @ingroup Search + */ +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 + */ + function termMatches() { + return array(); + } + + function numRows() { + return 0; + } + + /** + * Return true if results are included in this result set. + * STUB + * + * @return Boolean + */ + function hasResults() { + return false; + } + + /** + * Some search modes return a total hit count for the query + * in the entire article database. This may include pages + * in namespaces that would not be matched on the given + * settings. + * + * Return null if no total hits number is supported. + * + * @return Integer + */ + function getTotalHits() { + return null; + } + + /** + * Some search modes return a suggested alternate term if there are + * no exact hits. Returns true if there is one on this set. + * + * @return Boolean + */ + function hasSuggestion() { + return false; + } + + /** + * @return String: suggested query, null if none + */ + function getSuggestionQuery(){ + return null; + } + + /** + * @return String: HTML highlighted suggested query, '' if none + */ + function getSuggestionSnippet(){ + return ''; + } + + /** + * Return information about how and from where the results were fetched, + * should be useful for diagnostics and debugging + * + * @return String + */ + function getInfo() { + return null; + } + + /** + * Return a result set of hits on other (multiple) wikis associated with this one + * + * @return SearchResultSet + */ + function getInterwikiResults() { + return null; + } + + /** + * Check if there are results on other wikis + * + * @return Boolean + */ + function hasInterwikiResults() { + return $this->getInterwikiResults() != null; + } + + + /** + * Fetches next search result, or false. + * STUB + * + * @return SearchResult + */ + function next() { + return false; + } + + /** + * Frees the result set, if applicable. + */ + 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 + */ +class SearchResultTooMany { + ## Some search engines may bail out if too many matches are found +} + + +/** + * @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 { + var $mRevision = null; + var $mImage = null; + + function __construct( $row ) { + $this->mTitle = Title::makeTitle( $row->page_namespace, $row->page_title ); + if( !is_null($this->mTitle) ){ + $this->mRevision = Revision::newFromTitle( $this->mTitle ); + if( $this->mTitle->getNamespace() === NS_FILE ) + $this->mImage = wfFindFile( $this->mTitle ); + } + } + + /** + * Check if this is result points to an invalid title + * + * @return Boolean + */ + function isBrokenTitle(){ + if( is_null($this->mTitle) ) + return true; + return false; + } + + /** + * Check if target page is missing, happens when index is out of date + * + * @return Boolean + */ + function isMissingRevision(){ + return !$this->mRevision && !$this->mImage; + } + + /** + * @return Title + */ + function getTitle() { + return $this->mTitle; + } + + /** + * @return Double or null if not supported + */ + function getScore() { + return null; + } + + /** + * Lazy initialization of article text from DB + */ + protected function initText(){ + if( !isset($this->mText) ){ + if($this->mRevision != null) + $this->mText = $this->mRevision->getText(); + else // TODO: can we fetch raw wikitext for commons images? + $this->mText = ''; + + } + } + + /** + * @param $terms Array: terms to highlight + * @return String: highlighted text snippet, null (and not '') if not supported + */ + function getTextSnippet($terms){ + global $wgUser, $wgAdvancedSearchHighlighting; + $this->initText(); + list($contextlines,$contextchars) = SearchEngine::userHighlightPrefs($wgUser); + $h = new SearchHighlighter(); + if( $wgAdvancedSearchHighlighting ) + return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars ); + else + return $h->highlightSimple( $this->mText, $terms, $contextlines, $contextchars ); + } + + /** + * @param $terms Array: terms to highlight + * @return String: highlighted title, '' if not supported + */ + function getTitleSnippet($terms){ + return ''; + } + + /** + * @param $terms Array: terms to highlight + * @return String: highlighted redirect name (redirect to this page), '' if none or not supported + */ + function getRedirectSnippet($terms){ + return ''; + } + + /** + * @return Title object for the redirect to this page, null if none or not supported + */ + function getRedirectTitle(){ + return null; + } + + /** + * @return string highlighted relevant section name, null if none or not supported + */ + function getSectionSnippet(){ + return ''; + } + + /** + * @return Title object (pagename+fragment) for the section, null if none or not supported + */ + function getSectionTitle(){ + return null; + } + + /** + * @return String: timestamp + */ + function getTimestamp(){ + if( $this->mRevision ) + return $this->mRevision->getTimestamp(); + else if( $this->mImage ) + return $this->mImage->getTimestamp(); + return ''; + } + + /** + * @return Integer: number of words + */ + function getWordCount(){ + $this->initText(); + return str_word_count( $this->mText ); + } + + /** + * @return Integer: size in bytes + */ + function getByteSize(){ + $this->initText(); + return strlen( $this->mText ); + } + + /** + * @return Boolean if hit has related articles + */ + function hasRelated(){ + return false; + } + + /** + * @return String: interwiki prefix of the title (return iw even if title is broken) + */ + function getInterwikiPrefix(){ + return ''; + } +} + +/** + * Highlight bits of wikitext + * + * @ingroup Search + */ +class SearchHighlighter { + var $mCleanWikitext = true; + + function SearchHighlighter($cleanupWikitext = true){ + $this->mCleanWikitext = $cleanupWikitext; + } + + /** + * Default implementation of wikitext highlighting + * + * @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; + global $wgSearchHighlightBoundaries; + $fname = __METHOD__; + + if($text == '') + return ''; + + // spli text into text + templates/links/tables + $spat = "/(\\{\\{)|(\\[\\[[^\\]:]+:)|(\n\\{\\|)"; + // first capture group is for detecting nested templates/links/tables/references + $endPatterns = array( + 1 => '/(\{\{)|(\}\})/', // template + 2 => '/(\[\[)|(\]\])/', // image + 3 => "/(\n\\{\\|)|(\n\\|\\})/"); // table + + // FIXME: this should prolly be a hook or something + if(function_exists('wfCite')){ + $spat .= '|()'; // references via cite extension + $endPatterns[4] = '/()|(<\/ref>)/'; + } + $spat .= '/'; + $textExt = array(); // text extracts + $otherExt = array(); // other extracts + wfProfileIn( "$fname-split" ); + $start = 0; + $textLen = strlen($text); + $count = 0; // sequence number to maintain ordering + while( $start < $textLen ){ + // find start of template/image/table + if( preg_match( $spat, $text, $matches, PREG_OFFSET_CAPTURE, $start ) ){ + $epat = ''; + foreach($matches as $key => $val){ + if($key > 0 && $val[1] != -1){ + if($key == 2){ + // see if this is an image link + $ns = substr($val[0],2,-1); + if( $wgContLang->getNsIndex($ns) != NS_FILE ) + break; + + } + $epat = $endPatterns[$key]; + $this->splitAndAdd( $textExt, $count, substr( $text, $start, $val[1] - $start ) ); + $start = $val[1]; + break; + } + } + if( $epat ){ + // find end (and detect any nested elements) + $level = 0; + $offset = $start + 1; + $found = false; + while( preg_match( $epat, $text, $endMatches, PREG_OFFSET_CAPTURE, $offset ) ){ + if( array_key_exists(2,$endMatches) ){ + // found end + if($level == 0){ + $len = strlen($endMatches[2][0]); + $off = $endMatches[2][1]; + $this->splitAndAdd( $otherExt, $count, + substr( $text, $start, $off + $len - $start ) ); + $start = $off + $len; + $found = true; + break; + } else{ + // end of nested element + $level -= 1; + } + } else{ + // nested + $level += 1; + } + $offset = $endMatches[0][1] + strlen($endMatches[0][0]); + } + if( ! $found ){ + // couldn't find appropriate closing tag, skip + $this->splitAndAdd( $textExt, $count, substr( $text, $start, strlen($matches[0][0]) ) ); + $start += strlen($matches[0][0]); + } + continue; + } + } + // else: add as text extract + $this->splitAndAdd( $textExt, $count, substr($text,$start) ); + break; + } + + $all = $textExt + $otherExt; // these have disjunct key sets + + wfProfileOut( "$fname-split" ); + + // prepare regexps + foreach( $terms as $index => $term ) { + // manually do upper/lowercase stuff for utf-8 since PHP won't do it + if(preg_match('/[\x80-\xff]/', $term) ){ + $terms[$index] = preg_replace_callback('/./us',array($this,'caseCallback'),$terms[$index]); + } else { + $terms[$index] = $term; + } + } + $anyterm = implode( '|', $terms ); + $phrase = implode("$wgSearchHighlightBoundaries+", $terms ); + + // FIXME: a hack to scale contextchars, a correct solution + // would be to have contextchars actually be char and not byte + // length, and do proper utf-8 substrings and lengths everywhere, + // but PHP is making that very hard and unclean to implement :( + $scale = strlen($anyterm) / mb_strlen($anyterm); + $contextchars = intval( $contextchars * $scale ); + + $patPre = "(^|$wgSearchHighlightBoundaries)"; + $patPost = "($wgSearchHighlightBoundaries|$)"; + + $pat1 = "/(".$phrase.")/ui"; + $pat2 = "/$patPre(".$anyterm.")$patPost/ui"; + + wfProfileIn( "$fname-extract" ); + + $left = $contextlines; + + $snippets = array(); + $offsets = array(); + + // show beginning only if it contains all words + $first = 0; + $firstText = ''; + foreach($textExt as $index => $line){ + if(strlen($line)>0 && $line[0] != ';' && $line[0] != ':'){ + $firstText = $this->extract( $line, 0, $contextchars * $contextlines ); + $first = $index; + break; + } + } + if( $firstText ){ + $succ = true; + // check if first text contains all terms + foreach($terms as $term){ + if( ! preg_match("/$patPre".$term."$patPost/ui", $firstText) ){ + $succ = false; + break; + } + } + if( $succ ){ + $snippets[$first] = $firstText; + $offsets[$first] = 0; + } + } + if( ! $snippets ) { + // match whole query on text + $this->process($pat1, $textExt, $left, $contextchars, $snippets, $offsets); + // match whole query on templates/tables/images + $this->process($pat1, $otherExt, $left, $contextchars, $snippets, $offsets); + // match any words on text + $this->process($pat2, $textExt, $left, $contextchars, $snippets, $offsets); + // match any words on templates/tables/images + $this->process($pat2, $otherExt, $left, $contextchars, $snippets, $offsets); + + ksort($snippets); + } + + // add extra chars to each snippet to make snippets constant size + $extended = array(); + if( count( $snippets ) == 0){ + // couldn't find the target words, just show beginning of article + $targetchars = $contextchars * $contextlines; + $snippets[$first] = ''; + $offsets[$first] = 0; + } else{ + // if begin of the article contains the whole phrase, show only that !! + if( array_key_exists($first,$snippets) && preg_match($pat1,$snippets[$first]) + && $offsets[$first] < $contextchars * 2 ){ + $snippets = array ($first => $snippets[$first]); + } + + // calc by how much to extend existing snippets + $targetchars = intval( ($contextchars * $contextlines) / count ( $snippets ) ); + } + + foreach($snippets as $index => $line){ + $extended[$index] = $line; + $len = strlen($line); + if( $len < $targetchars - 20 ){ + // complete this line + if($len < strlen( $all[$index] )){ + $extended[$index] = $this->extract( $all[$index], $offsets[$index], $offsets[$index]+$targetchars, $offsets[$index]); + $len = strlen( $extended[$index] ); + } + + // add more lines + $add = $index + 1; + while( $len < $targetchars - 20 + && array_key_exists($add,$all) + && !array_key_exists($add,$snippets) ){ + $offsets[$add] = 0; + $tt = "\n".$this->extract( $all[$add], 0, $targetchars - $len, $offsets[$add] ); + $extended[$add] = $tt; + $len += strlen( $tt ); + $add++; + } + } + } + + //$snippets = array_map('htmlspecialchars', $extended); + $snippets = $extended; + $last = -1; + $extract = ''; + foreach($snippets as $index => $line){ + if($last == -1) + $extract .= $line; // first line + elseif($last+1 == $index && $offsets[$last]+strlen($snippets[$last]) >= strlen($all[$last])) + $extract .= " ".$line; // continous lines + else + $extract .= ' ... ' . $line; + + $last = $index; + } + if( $extract ) + $extract .= ' ... '; + + $processed = array(); + foreach($terms as $term){ + if( ! isset($processed[$term]) ){ + $pat3 = "/$patPre(".$term.")$patPost/ui"; // highlight word + $extract = preg_replace( $pat3, + "\\1\\2\\3", $extract ); + $processed[$term] = true; + } + } + + wfProfileOut( "$fname-extract" ); + + return $extract; + } + + /** + * Split text into lines and add it to extracts array + * + * @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 ); + foreach($split as $line){ + $tt = trim($line); + if( $tt ) + $extracts[$count++] = $tt; + } + } + + /** + * Do manual case conversion for non-ascii chars + * + * @param $matches Array + */ + function caseCallback($matches){ + global $wgContLang; + if( strlen($matches[0]) > 1 ){ + return '['.$wgContLang->lc($matches[0]).$wgContLang->uc($matches[0]).']'; + } else + return $matches[0]; + } + + /** + * Extract part of the text from start to end, but by + * not chopping up words + * @param $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; + + if( $start != 0) + $start = $this->position( $text, $start, 1 ); + if( $end >= strlen($text) ) + $end = strlen($text); + else + $end = $this->position( $text, $end ); + + if(!is_null($posStart)) + $posStart = $start; + if(!is_null($posEnd)) + $posEnd = $end; + + if($end > $start) + return substr($text, $start, $end-$start); + else + return ''; + } + + /** + * Find a nonletter near a point (index) in the text + * + * @param $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; + $s = max( 0, $point - $tolerance ); + $l = min( strlen($text), $point + $tolerance ) - $s; + $m = array(); + if( preg_match('/[ ,.!?~!@#$%^&*\(\)+=\-\\\|\[\]"\'<>]/', substr($text,$s,$l), $m, PREG_OFFSET_CAPTURE ) ){ + return $m[0][1] + $s + $offset; + } else{ + // check if point is on a valid first UTF8 char + $char = ord( $text[$point] ); + while( $char >= 0x80 && $char < 0xc0 ) { + // skip trailing bytes + $point++; + if($point >= strlen($text)) + return strlen($text); + $char = ord( $text[$point] ); + } + return $point; + + } + } + + /** + * Search extracts for a pattern, and return snippets + * + * @param $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 ){ + if($linesleft == 0) + return; // nothing to do + foreach($extracts as $index => $line){ + if( array_key_exists($index,$out) ) + continue; // this line already highlighted + + $m = array(); + if ( !preg_match( $pattern, $line, $m, PREG_OFFSET_CAPTURE ) ) + continue; + + $offset = $m[0][1]; + $len = strlen($m[0][0]); + if($offset + $len < $contextchars) + $begin = 0; + elseif( $len > $contextchars) + $begin = $offset; + else + $begin = $offset + intval( ($len - $contextchars) / 2 ); + + $end = $begin + $contextchars; + + $posBegin = $begin; + // basic snippet from this line + $out[$index] = $this->extract($line,$begin,$end,$posBegin); + $offsets[$index] = $posBegin; + $linesleft--; + if($linesleft == 0) + return; + } + } + + /** + * Basic wikitext removal + * @protected + */ + function removeWiki($text) { + $fname = __METHOD__; + wfProfileIn( $fname ); + + //$text = preg_replace("/'{2,5}/", "", $text); + //$text = preg_replace("/\[[a-z]+:\/\/[^ ]+ ([^]]+)\]/", "\\2", $text); + //$text = preg_replace("/\[\[([^]|]+)\]\]/", "\\1", $text); + //$text = preg_replace("/\[\[([^]]+\|)?([^|]]+)\]\]/", "\\2", $text); + //$text = preg_replace("/\\{\\|(.*?)\\|\\}/", "", $text); + //$text = preg_replace("/\\[\\[[A-Za-z_-]+:([^|]+?)\\]\\]/", "", $text); + $text = preg_replace("/\\{\\{([^|]+?)\\}\\}/", "", $text); + $text = preg_replace("/\\{\\{([^|]+\\|)(.*?)\\}\\}/", "\\2", $text); + $text = preg_replace("/\\[\\[([^|]+?)\\]\\]/", "\\1", $text); + $text = preg_replace_callback("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", array($this,'linkReplace'), $text); + //$text = preg_replace("/\\[\\[([^|]+\\|)(.*?)\\]\\]/", "\\2", $text); + $text = preg_replace("/<\/?[^>]+>/", "", $text); + $text = preg_replace("/'''''/", "", $text); + $text = preg_replace("/('''|<\/?[iIuUbB]>)/", "", $text); + $text = preg_replace("/''/", "", $text); + + wfProfileOut( $fname ); + return $text; + } + + /** + * callback to replace [[target|caption]] kind of links, if + * the target is category or image, leave it + * + * @param $matches Array + */ + function linkReplace($matches){ + $colon = strpos( $matches[1], ':' ); + if( $colon === false ) + return $matches[2]; // replace with caption + global $wgContLang; + $ns = substr( $matches[1], 0, $colon ); + $index = $wgContLang->getNsIndex($ns); + if( $index !== false && ($index == NS_FILE || $index == NS_CATEGORY) ) + return $matches[0]; // return the whole thing + else + return $matches[2]; + + } + + /** + * Simple & fast snippet extraction, but gives completely unrelevant + * snippets + * + * @param $text String + * @param $terms Array + * @param $contextlines Integer + * @param $contextchars Integer + * @return String + */ + public function highlightSimple( $text, $terms, $contextlines, $contextchars ) { + global $wgLang, $wgContLang; + $fname = __METHOD__; + + $lines = explode( "\n", $text ); + + $terms = implode( '|', $terms ); + $max = intval( $contextchars ) + 1; + $pat1 = "/(.*)($terms)(.{0,$max})/i"; + + $lineno = 0; + + $extract = ""; + wfProfileIn( "$fname-extract" ); + foreach ( $lines as $line ) { + if ( 0 == $contextlines ) { + break; + } + ++$lineno; + $m = array(); + if ( ! preg_match( $pat1, $line, $m ) ) { + continue; + } + --$contextlines; + $pre = $wgContLang->truncate( $m[1], -$contextchars ); + + if ( count( $m ) < 3 ) { + $post = ''; + } else { + $post = $wgContLang->truncate( $m[3], $contextchars ); + } + + $found = $m[2]; + + $line = htmlspecialchars( $pre . $found . $post ); + $pat2 = '/(' . $terms . ")/i"; + $line = preg_replace( $pat2, + "\\1", $line ); + + $extract .= "${line}\n"; + } + wfProfileOut( "$fname-extract" ); + + return $extract; + } + +} + +/** + * Dummy class to be used when non-supported Database engine is present. + * @todo Fixme: dummy class should probably try something at least mildly useful, + * such as a LIKE search through titles. + * @ingroup Search + */ +class SearchEngineDummy extends SearchEngine { + // no-op +} diff --git a/includes/search/SearchIBM_DB2.php b/includes/search/SearchIBM_DB2.php new file mode 100644 index 00000000..d7587186 --- /dev/null +++ b/includes/search/SearchIBM_DB2.php @@ -0,0 +1,224 @@ + +# 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 base class for IBM DB2 + * @ingroup Search + */ +class SearchIBM_DB2 extends SearchEngine { + function __construct($db) { + $this->db = $db; + } + + /** + * Perform a full text search query and return a result set. + * + * @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 SqlSearchResultSet($resultSet, $this->searchTerms); + } + + /** + * Perform a title-only search query and return a result set. + * + * @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 SqlSearchResultSet($resultSet, $this->searchTerms); + } + + + /** + * 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 ''; + $namespaces = implode(',', $this->namespaces); + if ($namespaces == '') { + $namespaces = '0'; + } + return 'AND page_namespace IN (' . $namespaces . ')'; + } + + /** + * Return a LIMIT clause to limit results on the query. + * @return String + */ + function queryLimit($sql) { + return $this->db->limitResult($sql, $this->limit, $this->offset); + } + + /** + * Does not do anything for generic search engine + * subclasses may define this though + * @return String + */ + function queryRanking($filteredTerm, $fulltext) { + // requires Net Search Extender or equivalent + // return ' ORDER BY score(1)'; + return ''; + } + + /** + * Construct the full SQL query to do the search. + * The guts shoulds be constructed in queryMain() + * @param string $filteredTerm String + * @param bool $fulltext Boolean + */ + function getQuery( $filteredTerm, $fulltext ) { + return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' . + $this->queryRedirect() . ' ' . + $this->queryNamespaces() . ' ' . + $this->queryRanking( $filteredTerm, $fulltext ) . ' '); + } + + + /** + * 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 string $filteredTerm String + * @param bool $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; + } + + /** @todo document */ + function parseQuery($filteredText, $fulltext) { + global $wgContLang; + $lc = SearchEngine::legalSearchChars(); + $this->searchTerms = array(); + + # FIXME: This doesn't handle parenthetical expressions. + $m = array(); + $q = array(); + + if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', + $filteredText, $m, PREG_SET_ORDER)) { + foreach($m as $terms) { + + // 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], '/' ); + if ($terms[4]) + $regexp .= "[0-9A-Za-z_]+"; + } else { + $regexp = preg_quote(str_replace('"', '', $terms[2]), '/'); + } + $this->searchTerms[] = $regexp; + } + } + + $searchon = $this->db->strencode(join(',', $q)); + $field = $this->getIndexField($fulltext); + + // requires Net Search Extender or equivalent + //return " CONTAINS($field, '$searchon') > 0 "; + + return " lcase($field) LIKE lcase('%$searchon%')"; + } + + /** + * 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' => $title, + 'si_text' => $text + ), 'SearchIBM_DB2::update' ); + // ? + //$dbw->query("CALL ctx_ddl.sync_index('si_text_idx')"); + //$dbw->query("CALL ctx_ddl.sync_index('si_title_idx')"); + } + + /** + * 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' => $title), + array('si_page' => $id), + 'SearchIBM_DB2::updateTitle', + array()); + } +} 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 @@ + +# 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/search/SearchMySQL4.php b/includes/search/SearchMySQL4.php new file mode 100644 index 00000000..3e2bb2d1 --- /dev/null +++ b/includes/search/SearchMySQL4.php @@ -0,0 +1,34 @@ + +# 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+ + * This class retained for backwards compatibility... + * The meat's been moved to SearchMySQL, since the 3.x variety is gone. + * @ingroup Search + * @deprecated + */ +class SearchMySQL4 extends SearchMySQL { + /* whee */ +} diff --git a/includes/search/SearchOracle.php b/includes/search/SearchOracle.php new file mode 100644 index 00000000..e4c5deee --- /dev/null +++ b/includes/search/SearchOracle.php @@ -0,0 +1,268 @@ + +# 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 base class for Oracle (ConText). + * @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; + } + + /** + * Perform a full text search query and return a result set. + * + * @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 SqlSearchResultSet($resultSet, $this->searchTerms); + } + + /** + * Perform a title-only search query and return a result set. + * + * @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); + } + + + /** + * 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 ''; + 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($sql) { + return $this->db->limitResult($sql, $this->limit, $this->offset); + } + + /** + * Does not do anything for generic search engine + * subclasses may define this though + * @return String + */ + function queryRanking($filteredTerm, $fulltext) { + return ' ORDER BY score(1)'; + } + + /** + * 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->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' . + $this->queryRedirect() . ' ' . + $this->queryNamespaces() . ' ' . + $this->queryRanking( $filteredTerm, $fulltext ) . ' '); + } + + + /** + * 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 page_id, page_namespace, page_title ' . + "FROM $page,$searchindex " . + 'WHERE page_id=si_page AND ' . $match; + } + + /** + * Parse a user input search string, and return an SQL fragment to be used + * as part of a WHERE clause + */ + function parseQuery($filteredText, $fulltext) { + global $wgContLang; + $lc = SearchEngine::legalSearchChars(); + $this->searchTerms = array(); + + # FIXME: This doesn't handle parenthetical expressions. + $m = array(); + $searchon = ''; + if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', + $filteredText, $m, PREG_SET_ORDER)) { + foreach($m as $terms) { + // 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]) + $regexp .= "[0-9A-Za-z_]+"; + } else { + $regexp = preg_quote(str_replace('"', '', $terms[2]), '/'); + } + $this->searchTerms[] = $regexp; + } + } + + + $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 $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' => $title, + 'si_text' => $text + ), 'SearchOracle::update' ); + $dbw->query("CALL ctx_ddl.sync_index('si_text_idx')"); + $dbw->query("CALL ctx_ddl.sync_index('si_title_idx')"); + } + + /** + * 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), + 'SearchOracle::updateTitle', + array()); + } + + + public static function legalSearchChars() { + return "\"" . parent::legalSearchChars(); + } +} diff --git a/includes/search/SearchPostgres.php b/includes/search/SearchPostgres.php new file mode 100644 index 00000000..0006fa82 --- /dev/null +++ b/includes/search/SearchPostgres.php @@ -0,0 +1,246 @@ + +# 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 base class for Postgres + * @ingroup Search + */ +class SearchPostgres extends SearchEngine { + + function __construct( $db ) { + $this->db = $db; + } + + /** + * Perform a full text search query via tsearch2 and return a result set. + * Currently searches a page's current title (page.page_title) and + * latest revision article text (pagecontent.old_text) + * + * @param $term String: raw search term + * @return PostgresSearchResultSet + */ + function searchTitle( $term ) { + $q = $this->searchQuery( $term , 'titlevector', 'page_title' ); + $olderror = error_reporting(E_ERROR); + $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) ); + error_reporting($olderror); + if (!$resultSet) { + // Needed for "Query requires full scan, GIN doesn't support it" + return new SearchResultTooMany(); + } + return new PostgresSearchResultSet( $resultSet, $this->searchTerms ); + } + function searchText( $term ) { + $q = $this->searchQuery( $term, 'textvector', 'old_text' ); + $olderror = error_reporting(E_ERROR); + $resultSet = $this->db->resultObject( $this->db->query( $q, 'SearchPostgres', true ) ); + error_reporting($olderror); + if (!$resultSet) { + return new SearchResultTooMany(); + } + return new PostgresSearchResultSet( $resultSet, $this->searchTerms ); + } + + + /* + * Transform the user's search string into a better form for tsearch2 + * Returns an SQL fragment consisting of quoted text to search for. + */ + function parseQuery( $term ) { + + wfDebug( "parseQuery received: $term \n" ); + + ## No backslashes allowed + $term = preg_replace('/\\\/', '', $term); + + ## Collapse parens into nearby words: + $term = preg_replace('/\s*\(\s*/', ' (', $term); + $term = preg_replace('/\s*\)\s*/', ') ', $term); + + ## Treat colons as word separators: + $term = preg_replace('/:/', ' ', $term); + + $searchstring = ''; + $m = array(); + if( preg_match_all('/([-!]?)(\S+)\s*/', $term, $m, PREG_SET_ORDER ) ) { + foreach( $m as $terms ) { + if (strlen($terms[1])) { + $searchstring .= ' & !'; + } + if (strtolower($terms[2]) === 'and') { + $searchstring .= ' & '; + } + else if (strtolower($terms[2]) === 'or' or $terms[2] === '|') { + $searchstring .= ' | '; + } + else if (strtolower($terms[2]) === 'not') { + $searchstring .= ' & !'; + } + else { + $searchstring .= " & $terms[2]"; + } + } + } + + ## Strip out leading junk + $searchstring = preg_replace('/^[\s\&\|]+/', '', $searchstring); + + ## Remove any doubled-up operators + $searchstring = preg_replace('/([\!\&\|]) +(?:[\&\|] +)+/', "$1 ", $searchstring); + + ## Remove any non-spaced operators (e.g. "Zounds!") + $searchstring = preg_replace('/([^ ])[\!\&\|]/', "$1", $searchstring); + + ## Remove any trailing whitespace or operators + $searchstring = preg_replace('/[\s\!\&\|]+$/', '', $searchstring); + + ## Remove unnecessary quotes around everything + $searchstring = preg_replace('/^[\'"](.*)[\'"]$/', "$1", $searchstring); + + ## Quote the whole thing + $searchstring = $this->db->addQuotes($searchstring); + + wfDebug( "parseQuery returned: $searchstring \n" ); + + return $searchstring; + + } + + /** + * Construct the full SQL query to do the search. + * @param $filteredTerm String + * @param $fulltext String + */ + function searchQuery( $term, $fulltext, $colname ) { + global $wgDBversion; + + if ( !isset( $wgDBversion ) ) { + $this->db->getServerVersion(); + $wgDBversion = $this->db->numeric_version; + } + $prefix = $wgDBversion < 8.3 ? "'default'," : ''; + + # Get the SQL fragment for the given term + $searchstring = $this->parseQuery( $term ); + + ## We need a separate query here so gin does not complain about empty searches + $SQL = "SELECT to_tsquery($prefix $searchstring)"; + $res = $this->db->doQuery($SQL); + if (!$res) { + ## TODO: Better output (example to catch: one 'two) + die ("Sorry, that was not a valid search string. Please go back and try again"); + } + $top = pg_fetch_result($res,0,0); + + if ($top === "") { ## e.g. if only stopwords are used XXX return something better + $query = "SELECT page_id, page_namespace, page_title, 0 AS score ". + "FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " . + "AND r.rev_text_id = c.old_id AND 1=0"; + } + else { + $m = array(); + if( preg_match_all("/'([^']+)'/", $top, $m, PREG_SET_ORDER ) ) { + foreach( $m as $terms ) { + $this->searchTerms[$terms[1]] = $terms[1]; + } + } + + $rankscore = $wgDBversion > 8.2 ? 5 : 1; + $rank = $wgDBversion < 8.3 ? 'rank' : 'ts_rank'; + $query = "SELECT page_id, page_namespace, page_title, ". + "$rank($fulltext, to_tsquery($prefix $searchstring), $rankscore) AS score ". + "FROM page p, revision r, pagecontent c WHERE p.page_latest = r.rev_id " . + "AND r.rev_text_id = c.old_id AND $fulltext @@ to_tsquery($prefix $searchstring)"; + } + + ## Redirects + if (! $this->showRedirects) + $query .= ' AND page_is_redirect = 0'; + + ## Namespaces - defaults to 0 + if( !is_null($this->namespaces) ){ // null -> search all + if ( count($this->namespaces) < 1) + $query .= ' AND page_namespace = 0'; + else { + $namespaces = $this->db->makeList( $this->namespaces ); + $query .= " AND page_namespace IN ($namespaces)"; + } + } + + $query .= " ORDER BY score DESC, page_id DESC"; + + $query .= $this->db->limitResult( '', $this->limit, $this->offset ); + + wfDebug( "searchQuery returned: $query \n" ); + + return $query; + } + + ## Most of the work of these two functions are done automatically via triggers + + function update( $pageid, $title, $text ) { + ## We don't want to index older revisions + $SQL = "UPDATE pagecontent SET textvector = NULL WHERE old_id IN ". + "(SELECT rev_text_id FROM revision WHERE rev_page = " . intval( $pageid ) . + " ORDER BY rev_text_id DESC OFFSET 1)"; + $this->db->doQuery($SQL); + return true; + } + + function updateTitle( $id, $title ) { + return true; + } + +} ## end of the SearchPostgres class + +/** + * @ingroup Search + */ +class PostgresSearchResult extends SearchResult { + function __construct( $row ) { + parent::__construct($row); + $this->score = $row->score; + } + function getScore() { + return $this->score; + } +} + +/** + * @ingroup Search + */ +class PostgresSearchResultSet extends SqlSearchResultSet { + function __construct( $resultSet, $terms ) { + parent::__construct( $resultSet, $terms ); + } + + function next() { + $row = $this->mResultSet->fetchObject(); + if( $row === false ) { + return false; + } else { + return new PostgresSearchResult( $row ); + } + } +} 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 @@ +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/search/SearchUpdate.php b/includes/search/SearchUpdate.php new file mode 100644 index 00000000..e30c70e6 --- /dev/null +++ b/includes/search/SearchUpdate.php @@ -0,0 +1,113 @@ +mId = $id; + $this->mText = $text; + + $this->mNamespace = $nt->getNamespace(); + $this->mTitle = $nt->getText(); # Discard namespace + + $this->mTitleWords = $this->mTextWords = array(); + } else { + wfDebug( "SearchUpdate object created with invalid title '$title'\n" ); + } + } + + function doUpdate() { + global $wgContLang, $wgDisableSearchUpdate; + + if( $wgDisableSearchUpdate || !$this->mId ) { + return false; + } + $fname = 'SearchUpdate::doUpdate'; + wfProfileIn( $fname ); + + $search = SearchEngine::create(); + $lc = SearchEngine::legalSearchChars() . '&#;'; + + if( $this->mText === false ) { + $search->updateTitle($this->mId, + Title::indexTitle( $this->mNamespace, $this->mTitle )); + wfProfileOut( $fname ); + return; + } + + # Language-specific strip/conversion + $text = $wgContLang->normalizeForSearch( $this->mText ); + + wfProfileIn( $fname.'-regexps' ); + $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 + + # Strip external URLs + $uc = "A-Za-z0-9_\\/:.,~%\\-+&;#?!=()@\\xA0-\\xFF"; + $protos = "http|https|ftp|mailto|news|gopher"; + $pat = "/(^|[^\\[])({$protos}):[{$uc}]+([^{$uc}]|$)/"; + $text = preg_replace( $pat, "\\1 \\3", $text ); + + $p1 = "/([^\\[])\\[({$protos}):[{$uc}]+]/"; + $p2 = "/([^\\[])\\[({$protos}):[{$uc}]+\\s+([^\\]]+)]/"; + $text = preg_replace( $p1, "\\1 ", $text ); + $text = preg_replace( $p2, "\\1 \\3 ", $text ); + + # Internal image links + $pat2 = "/\\[\\[image:([{$uc}]+)\\.(gif|png|jpg|jpeg)([^{$uc}])/i"; + $text = preg_replace( $pat2, " \\1 \\3", $text ); + + $text = preg_replace( "/([^{$lc}])([{$lc}]+)]]([a-z]+)/", + "\\1\\2 \\2\\3", $text ); # Handle [[game]]s + + # Strip all remaining non-search characters + $text = preg_replace( "/[^{$lc}]+/", " ", $text ); + + # Handle 's, s' + # + # $text = preg_replace( "/([{$lc}]+)'s /", "\\1 \\1's ", $text ); + # $text = preg_replace( "/([{$lc}]+)s' /", "\\1s ", $text ); + # + # These tail-anchored regexps are insanely slow. The worst case comes + # when Japanese or Chinese text (ie, no word spacing) is written on + # a wiki configured for Western UTF-8 mode. The Unicode characters are + # expanded to hex codes and the "words" are very long paragraph-length + # monstrosities. On a large page the above regexps may take over 20 + # seconds *each* on a 1GHz-level processor. + # + # Following are reversed versions which are consistently fast + # (about 3 milliseconds on 1GHz-level processor). + # + $text = strrev( preg_replace( "/ s'([{$lc}]+)/", " s'\\1 \\1", strrev( $text ) ) ); + $text = strrev( preg_replace( "/ 's([{$lc}]+)/", " s\\1", strrev( $text ) ) ); + + # Strip wiki '' and ''' + $text = preg_replace( "/''[']*/", " ", $text ); + wfProfileOut( "$fname-regexps" ); + + wfRunHooks( 'SearchUpdate', array( $this->mId, $this->mNamespace, $this->mTitle, &$text ) ); + + # Perform the actual update + $search->update($this->mId, Title::indexTitle( $this->mNamespace, $this->mTitle ), + $text); + + wfProfileOut( $fname ); + } +} + +/** + * Placeholder class + * @ingroup Search + */ +class SearchUpdateMyISAM extends SearchUpdate { + # Inherits everything +} 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 @@ +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 ) . '
      ';# 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' ) ) . '
      '; + + $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', - 'HTML', - 'XML' . - '
      ' . htmlspecialchars( $navText ) . '
      ' - ) ) ); - } else if ( $ot == 'xml' ) { - $wgOut->disable(); - header( 'Content-type: text/xml' ); - echo wfAllMessagesMakeXml( $messages ); - } else { - $wgOut->addHTML( $wgLang->pipeList( array( - 'PHP', - 'HTML', - 'XML' - ) ) ); - $wgOut->addWikiText( $navText ); - $wgOut->addHTML( wfAllMessagesMakeHTMLText( $messages ) ); - } - wfProfileOut( __METHOD__ . '-output' ); - - wfProfileOut( __METHOD__ ); -} -function wfAllMessagesMakeXml( &$messages ) { - global $wgLang; - $lang = $wgLang->getCode(); - $txt = "\n"; - $txt .= "\n"; - foreach( $messages as $key => $m ) { - $txt .= "\t" . Xml::element( 'message', array( 'name' => $key ), $m['msg'] ) . "\n"; - $messages[$key] = NULL; // trade bytes - } - $txt .= ""; - 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" . + '
    ' . + Xml::label( wfMsg( 'allmessages-prefix' ), 'mw-allmessages-form-prefix' ) . + "" . + Xml::input( 'prefix', 20, str_replace( '_', ' ', $this->prefix ), array( 'id' => 'mw-allmessages-form-prefix' ) ) . + "
    " . + wfMsg( 'allmessages-filter' ) . + "" . + 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 ) + ) . + "
    " . + Xml::label( wfMsg( 'allmessages-language' ), 'mw-allmessages-form-lang' ) . + "" . + 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( '/(?\n +
    " . + Xml::submitButton( wfMsg( 'allmessages-filter-submit' ) ) . + "
    - - - - - - - '; - - 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, "" . - htmlspecialchars( $key ) . '' ); + 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, "" . - htmlspecialchars( $key ) . '' ); + $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 = ""; - - if( $changed ) { - $txt .= " - - - - - "; + // 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 .= " - - - "; + $this->suffix = ''; } - $messages[$key] = NULL; // trade bytes - $i++; } - $txt .= '
    ' . wfMsgHtml( 'allmessagesname' ) . '' . wfMsgHtml( 'allmessagesdefault' ) . '
    ' . wfMsgHtml( 'allmessagescurrent' ) . '
    - $anchor$pageLink
    $talkLink -
    - $message -
    - $mw -
    - $anchor$pageLink
    $talkLink -
    - $mw -
    '; - 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" . + " + " . + wfMsg( 'allmessagesname' ) . " + + " . + wfMsg( 'allmessagesdefault' ) . + " + \n + + " . + wfMsg( 'allmessagescurrent' ) . + " + \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 = ' '; + } + $s .= Xml::tags( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) + . "\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 .= " - " . - Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) . - " - " . - Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . - " - - - " . - Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) . - " - " . - Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) . - " - - - " . - Xml::label( wfMsg( 'namespace' ), 'namespace' ) . - " - " . - Xml::namespaceSelector( $namespace, null ) . ' ' . - Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . - " - "; - $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 .= " + " . + Xml::label( wfMsg( 'allpagesfrom' ), 'nsfrom' ) . + " + " . + Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . + " + + + " . + Xml::label( wfMsg( 'allpagesto' ), 'nsto' ) . + " + " . + Xml::input( 'to', 30, str_replace('_',' ',$to), array( 'id' => 'nsto' ) ) . + " + + + " . + Xml::label( wfMsg( 'namespace' ), 'namespace' ) . + " + " . + Xml::namespaceSelector( $namespace, null ) . ' ' . + Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . + " +"; + $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 .= ""; + $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 .= '
    '; + $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 = ''; - $out2 .= '
    ' . $nsForm; - $out2 .= '' . - $wgUser->getSkin()->makeKnownLinkObj( $this->getTitle(), wfMsgHtml ( 'allpages' ) ); - $out2 .= "
    "; + $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ). + ' + ' . + $nsForm . + ' + ' . + $wgUser->getSkin()->link( $this->getTitle(), wfMsgHtml ( 'allpages' ), + array(), array(), 'known' ) . + " + " . + 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 { "$inpointf", "$outpointf" ); - return ''.$out.''; + return '' . $out . ''; } /** @@ -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 = ''; - + $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 ? '
    ' : '' ) . - $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . + $sk->linkKnown( $t, htmlspecialchars( $t->getText() ) ) . ($s->page_is_redirect ? '
    ' : '' ); } else { $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; @@ -316,13 +318,13 @@ class SpecialAllpages extends IncludableSpecialPage { $out .= ""; $n++; if( $n % 3 == 0 ) { - $out .= ''; + $out .= "\n"; } } if( ($n % 3) != 0 ) { - $out .= ''; + $out .= "\n"; } - $out .= '
    $link
    '; + $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 = ''; - $out2 .= ' + +
    ' . $nsForm; - $out2 .= '' . - $sk->makeKnownLinkObj( $self, - wfMsgHtml ( 'allpages' ) ); + $out2 = Xml::openElement( 'table', array( 'class' => 'mw-allpages-table-form' ) ). + '
    ' . + $nsForm . + '' . + $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 .= "
    "; @@ -399,7 +419,7 @@ class SpecialAllpages extends IncludableSpecialPage { $wgOut->addHTML( $out2 . $out ); if( isset($prevLink) or isset($nextLink) ) { - $wgOut->addHTML( '

    ' ); + $wgOut->addHTML( '


    ' ); 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 @@ 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' ) ) . " {$mIpaddress} " . - 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' ) ) ). " " ); - if ( $showblockoptions ) { + if( $showblockoptions ) { $wgOut->addHTML(" {$mIpbexpiry} @@ -204,8 +233,12 @@ class IPBlockForm { {$mIpbreason} " . - 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() ) ) . " @@ -234,36 +267,37 @@ class IPBlockForm { " ); - global $wgSysopEmailBans, $wgBlockAllowsUTEdit; - if ( $wgSysopEmailBans && $wgUser->isAllowed( 'blockemail' ) ) { + if( self::canBlockEmail( $wgUser ) ) { $wgOut->addHTML("   " . Xml::checkLabel( wfMsg( 'ipbemailban' ), 'wpEmailBan', 'wpEmailBan', $this->BlockEmail, - array( 'tabindex' => '9' )) . " + array( 'tabindex' => '9' ) ) . " " ); } // Allow some users to hide name from block log, blocklist and listusers - if ( $wgUser->isAllowed( 'hideuser' ) ) { + if( $wgUser->isAllowed( 'hideuser' ) ) { $wgOut->addHTML("   " . Xml::checkLabel( wfMsg( 'ipbhidename' ), 'wpHideName', 'wpHideName', $this->BlockHideName, - array( 'tabindex' => '10' ) ) . " + array( 'tabindex' => '10' ) + ) . " " ); } - - # Watchlist their user page? - $wgOut->addHTML(" + + # Watchlist their user page? (Only if user is logged in) + if( $wgUser->isLoggedIn() ) { + $wgOut->addHTML("   " . @@ -272,7 +306,11 @@ class IPBlockForm { array( 'tabindex' => '11' ) ) . " " - ); + ); + } + + # Can we explicitly disallow the use of user_talk? + global $wgBlockAllowsUTEdit; if( $wgBlockAllowsUTEdit ){ $wgOut->addHTML(" @@ -313,13 +351,23 @@ 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 '

    '; } - + /** * 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 * @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( '
    $1
    ', 'booksources-invalid-isbn' ); + $wgOut->wrapWikiMsg( "
    \n$1
    ", '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 '' . $skin->makeLinkObj( $fromObj ) . ''; + return '' . $skin->link( $fromObj ) . ''; } - $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() . '
      ' . $cap->getBody() . '
    ' . @@ -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( "
    $1
    ", 'confirmemail_pending' ); + $wgOut->wrapWikiMsg( "
    \n$1
    ", '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( + '

    ' . $pager->getNavigationBar() . '

    ' . + $pager->getBody() . + '

    ' . $pager->getNavigationBar() . '

    ' + ); + } - # Show a message about slave lag, if applicable - if( ( $lag = $pager->getDatabase()->getLag() ) > 0 ) - $wgOut->showLagWarning( $lag ); - $wgOut->addHTML( - '

    ' . $pager->getNavigationBar() . '

    ' . - $pager->getBody() . - '

    ' . $pager->getNavigationBar() . '

    ' - ); + # 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( '' ); + $text = wfMsgNoTrans( $message, $target ); + if( !wfEmptyMsg( $message, $text ) && $text != '-' ) { + $wgOut->wrapWikiMsg( + "", + 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 .= '
    ' . 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 ) . '
    ' . - 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' ) ) ) . ' '. '' . 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 .= "

    {$explain}

    "; + $f .= "

    {$explain}

    "; $f .= '
    ' . 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 .= '' . $this->messages['uctop'] . ''; - 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 .= '' . $this->messages['uctop'] . ''; + # 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 = '' . $date . ''; + } 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 = '' . $d . ''; - } - if( $rev->getParentId() === 0 ) { - $nflag = '' . $this->messages['newpageletter'] . ''; + $nflag = ChangesList::flag( 'newpage' ); } else { $nflag = ''; } if( $rev->isMinor() ) { - $mflag = '' . $this->messages['minoreditletter'] . ' '; + $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 .= " " . wfMsgHtml('rev-deleted-user-contribs') . ""; } # 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() . - '×tamp=' . $rev->getTimestamp() ); + $reviewlink = $sk->linkKnown( + SpecialPage::getTitleFor( 'Undelete', $page->getPrefixedDBkey() ), + $this->messages['undeleteviewlink'] + ); - $last = $sk->makeKnownLinkObj( $undelete, - $this->messages['diff'], - "target=" . $page->getPrefixedUrl() . - "×tamp=" . $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 = '' . $d . ''; + if( !$wgUser->isAllowed('undelete') || !$rev->userCan(Revision::DELETED_TEXT) ) { + $link = $date; // unusable link } else { - $link = $sk->makeKnownLinkObj( $undelete, $d, - 'target=' . $page->getPrefixedUrl() . - '×tamp=' . $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 = '' . $link . ''; } - $pagelink = $sk->makeLinkObj( $page ); + $pagelink = $sk->link( $page ); if( $rev->isMinor() ) { - $mflag = '' . $this->messages['minoreditletter'] . ' '; + $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 .= " " . wfMsgHtml('rev-deleted-user-contribs') . ""; } - $ret = "
  • $ret
  • \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( '' ); + $wgOut->wrapWikiMsg( "", 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 '' . $skin->makeLinkObj( $titleA, '', 'redirect=no' ) . ''; + return '' . $skin->link( $titleA, null, array(), array( 'redirect' => 'no' ) ) . ''; } $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 ) . ' '; $form .= Xml::submitButton( wfMsg( 'export-addcat' ), array( 'name' => 'addcat' ) ) . '
    '; if ( $wgExportFromNamespaces ) { - $form .= Xml::namespaceSelector( '', null, 'nsindex', wfMsg( 'export-addnstext' ) ) . ' '; + $form .= Xml::namespaceSelector( $nsindex, null, 'nsindex', wfMsg( 'export-addnstext' ) ) . ' '; $form .= Xml::submitButton( wfMsg( 'export-addns' ), array( 'name' => 'addns' ) ) . '
    '; } @@ -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 ''; + } + $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( '

    ' . - wfMsgHtml( 'fileduplicatesearch-result-1', $filename ) . - '

    ' + $wgOut->wrapWikiMsg( + "

    \n$1\n

    ", + array( 'fileduplicatesearch-result-1', $filename ) ); } elseif ( $count > 1 ) { - $wgOut->addHTML( '

    ' . - wfMsgExt( 'fileduplicatesearch-result-n', array( 'parseinline' ), $filename, $wgLang->formatNum( $count - 1 ) ) . - '

    ' + $wgOut->wrapWikiMsg( + "

    \n$1\n

    ", + 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( "
  • " . $skin->makeKnownLinkObj( $title ) . " " . + $wgOut->addHTML( "
  • " . $skin->linkKnown( $title ) . " " . wfMsgExt( 'import-revision-count', array( 'parsemag', 'escape' ), $localCount ) . "
  • \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( '
  • ' . wfMsgHtml( 'import-nonewrevisions' ) . '
  • ' ); + $wgOut->addHTML( "
  • " . $skin->linkKnown( $title ) . " " . + wfMsgHtml( 'import-nonewrevisions' ) . "
  • \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/. * @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', '' . $wgLang->commaList( $wgUrlProtocols) . '' ) ); + $wgOut->addWikiMsg( 'linksearch-text', '' . $wgLang->commaList( $wgUrlProtocols ) . '' ); $s = Xml::openElement( 'form', array( 'id' => 'mw-linksearch-form', 'method' => 'get', 'action' => $GLOBALS['wgScript'] ) ) . Xml::hidden( 'title', $self->getPrefixedDbKey() ) . '
    ' . @@ -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 @@ -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 .= "
    "; - $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); - $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-type' ), 'type' ) . ' ' . - self::typeSelector( 'type', $wgRequest->getVal( 'type' ), 'type' ); - $s .= ' '; - $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-user' ), 'user', 'user', - false, $wgRequest->getVal( 'user' ) ); - $s .= '

    '; - $s .= Xml::label( wfMsgHtml( 'listuserrestrictions-namespace' ), 'namespace' ) . ' ' . - Xml::namespaceSelector( $wgRequest->getVal( 'namespace' ), '', 'namespace' ); - $s .= ' '; - $s .= Xml::inputLabel( wfMsgHtml( 'listuserrestrictions-page' ), 'page', 'page', - false, $wgRequest->getVal( 'page' ) ); - $s .= Xml::submitButton( wfMsg( 'listuserrestrictions-submit' ) ); - $s .= "

    "; - 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 "
  • {$timestamp}, {$blockerlink} {$msg} {$reason} {$removelink}
  • \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' ) ) . '' . 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 = '
    ' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), '' ); + $grouplink = '
    ' . $this->skin->link( + SpecialPage::getTitleFor( 'Listusers' ), + wfMsgHtml( 'listgrouprights-members' ), + array(), + array(), + array( 'known', 'noclasses' ) + ); } elseif ( !in_array( $group, $wgImplicitGroups ) ) { - $grouplink = '
    ' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Listusers' ), wfMsgHtml( 'listgrouprights-members' ), 'group=' . $group ); + $grouplink = '
    ' . $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( ' @@ -82,30 +101,47 @@ class SpecialListGroupRights extends SpecialPage { $grouppage . $grouplink . ' ' . - self::formatPermissions( $permissions, $addgroups, $removegroups ) . + self::formatPermissions( $permissions, $revoke, $addgroups, $removegroups, $addgroupsSelf, $removegroupsSelf ) . ' ' ); } $wgOut->addHTML( - Xml::closeElement( 'table' ) . "\n" + Xml::closeElement( 'table' ) . "\n

    \n" ); + $wgOut->wrapWikiMsg( "
    \n$1\n
    ", '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 + '' . $permission . '' + ); + $r[] = $description; + } + } + foreach( $revoke as $permission => $revoked ) { + if( $revoked ) { + $description = wfMsgExt( 'listgrouprights-right-revoked', array( 'parseinline' ), + User::getRightDescription( $permission ), + '' . $permission . '' ); $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 "$rd_link"; 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 = "$item"; + } 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' ) . '
    '; + $out .= Xml::closeElement( 'select' ) . '
    '; $out .= Xml::checkLabel( wfMsg('listusers-editsonly'), 'editsOnly', 'editsOnly', $this->editsOnly ); $out .= ' '; $out .= Xml::checkLabel( wfMsg('listusers-creationsort'), 'creationSort', 'creationSort', $this->creationSort ); - $out .= '
    '; + $out .= '
    '; 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( '

    ' . htmlspecialchars( $err ) . "

    \n" ); } @@ -65,7 +65,7 @@ class DBLockForm { $reason = htmlspecialchars( $this->reason ); $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( <<addHTML( << {$elr}: @@ -85,7 +85,7 @@ class DBLockForm { -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 = '' . $pageLink . ''; } @@ -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 '' . $wgLang->timeanddate( $ts, true ) . ''; } else { - $link = $this->sk->makeKnownLinkObj( $titleObj, - $wgLang->timeanddate( $ts, true ), "target=$target×tamp=$ts" ); + $link = $this->sk->linkKnown( + $titleObj, + $wgLang->timeanddate( $ts, true ), + array(), + array( + 'target' => $target, + 'timestamp' => $ts + ) + ); if( $this->isDeleted($row, Revision::DELETED_TEXT) ) $link = '' . $link . ''; 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 ''; + } + $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 { "; $err = ''; } else { + if ($this->oldTitle->getNamespace() == NS_USER && !$this->oldTitle->isSubpage() ) { + $wgOut->wrapWikiMsg( "
    \n$1\n
    ", '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( "
    \n" ); + $wgOut->addWikiMsg( $noticeMsg ); + LogEventsList::showLogExtract( $wgOut, 'protect', $this->oldTitle->getPrefixedText(), '', array( 'lim' => 1 ) ); + $wgOut->addHTML( "
    \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' ) . " " . - 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() ) . " @@ -193,7 +221,8 @@ class MovePageForm { Xml::label( wfMsg( 'movereason' ), 'wpReason' ) . " " . - 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 ) . " " ); @@ -242,34 +271,43 @@ class MovePageForm { " . - 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' ) + ) . ' ' . + 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) + ) ) . " " ); } - $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( " " . Xml::checkLabel( wfMsg( 'move-watch' ), 'wpWatch', 'watch', $watchChecked ) . " - + "); + } + + $wgOut->addHTML( " {$confirm}   @@ -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
    \n".$wgLang->timeanddate( $s->img_timestamp, true )."
    \n" ); + $gallery->add( $nt, "$ul
    \n".htmlspecialchars($wgLang->timeanddate( $s->img_timestamp, true ))."
    \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 = '

    ' . $botLink . ' '. wfMsgHtml( 'viewprevnext', $prevLink, $nextLink, $dateLink ) .'

    '; 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 @@ 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( + '
    $1
    ', + 'savedprefs' + ); } - - $msg = ''; - if ( !wfRunHooks( 'SavePreferences', array( $this, $wgUser, &$msg, $oldOptions ) ) ) { - $this->mainPrefsForm( 'error', $msg ); - return; + + if ( $wgRequest->getCheck( 'eauth' ) ) { + $wgOut->wrapWikiMsg( "
    \n$1
    ", + '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 .= "
    \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 "
    " . - " $trailer
    \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 "$td1$td2"; - } - - /** - * 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( '
    $1
    ', 'savedprefs' ); - } else if ( 'error' == $status ) { - $wgOut->addWikiText( '
    ' . $message . '
    ' ); - } 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 ).'
    '; - $disableEmailPrefs = false; - } else { - $disableEmailPrefs = true; - $skin = $wgUser->getSkin(); - $emailauthenticated = wfMsg('emailnotauthenticated').'
    ' . - $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Confirmemail' ), - wfMsg( 'emailconfirmlink' ) ) . '
    '; - } - } else { - $emailauthenticated = ''; - $disableEmailPrefs = false; - } - - if ($this->mUserEmail == '') { - $emailauthenticated = wfMsg( 'noemailprefs' ) . '
    '; - } - - $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 ) : ''; - - # - - $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 ) . - '
    (' . $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( - ' ', - 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( - ' ', - 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( ' ', $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( '
    • ' . $link . '
    ' ) ); - } - - # - # 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 = - " " . - $this->getToggle( 'ccmeonemails', '', $disableEmailPrefs ); - } - - - $wgOut->addHTML( - $this->tableRow( Xml::element( 'h2', null, wfMsg( 'email' ) ) ) . - $this->tableRow( - $emailauthenticated. - $enotifrevealaddr. - $enotifwatchlistpages. - $enotifusertalkpages. - $enotifminoredits. - $moreEmail - ) - ); - } - # - - $wgOut->addHTML( - Xml::closeElement( 'table' ) . - Xml::closeElement( 'fieldset' ) - ); - - - # Quickbar - # - if ($this->mSkin == 'cologneblue' || $this->mSkin == 'standard') { - $wgOut->addHTML( "
    \n" . wfMsg( 'qbsettings' ) . "\n" ); - for ( $i = 0; $i < count( $qbs ); ++$i ) { - if ( $i == $this->mQuickbar ) { $checked = ' checked="checked"'; } - else { $checked = ""; } - $wgOut->addHTML( "
    \n" ); - } - $wgOut->addHTML( "
    \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( "
    \n\n" . wfMsg( 'skin' ) . "\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 = "($previewtext)"; - $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( " - $previewlink{$extraLinks}
    \n" ); - } - $wgOut->addHTML( "
    \n\n" ); - } - - # Math - # - global $wgUseTeX; - if( $wgUseTeX ) { - $wgOut->addHTML( "
    \n" . wfMsg('math') . '' ); - 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( "
    \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' ) . - ' - ' . - Xml::label( wfMsg( 'imagemaxsize' ), $imageSizeId ) . - ' - ' . - Xml::openElement( 'select', array( 'name' => $imageSizeId, 'id' => $imageSizeId ) ) . - $imageLimitOptions . - Xml::closeElement( 'select' ) . - ' - - ' . - Xml::label( wfMsg( 'thumbsize' ), $thumbSizeId ) . - ' - ' . - Xml::openElement( 'select', array( 'name' => $thumbSizeId, 'id' => $thumbSizeId ) ) . - $imageThumbOptions . - Xml::closeElement( 'select' ) . - ' - ' . - 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)' ) ) ) . - " - - " . - Xml::element( 'input', - array( 'type' => 'button', - 'value' => wfMsg( 'guesstimezone' ), - 'onclick' => 'javascript:guessTimezone()', - 'id' => 'guesstimezonebutton', - 'style' => 'display:none;' ) ) . - " - " . - 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' ) . - ' - ' . - Xml::label( wfMsg( 'recentchangesdays' ), 'wpRecentDays' ) . - ' - ' . - Xml::input( 'wpRecentDays', 3, $this->mRecentDays, array( 'id' => 'wpRecentDays' ) ) . ' ' . - wfMsgExt( 'recentchangesdays-max', 'parsemag', - $wgLang->formatNum( ceil( $wgRCMaxAge / ( 3600 * 24 ) ) ) ) . - ' - - ' . - Xml::label( wfMsg( 'recentchangescount' ), 'wpRecent' ) . - ' - ' . - Xml::input( 'wpRecent', 3, $this->mRecent, array( 'id' => 'wpRecent' ) ) . - ' - ' . - Xml::closeElement( 'table' ) . - '
    ' - ); - - $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' ) . - '

    ' . - $this->getToggle( 'extendwatchlist' ) . - Xml::inputLabel( wfMsg( 'prefs-watchlist-edits' ), 'wpWatchlistEdits', 'wpWatchlistEdits', 3, $this->mWatchlistEdits ) . ' ' . - wfMsgHTML( 'prefs-watchlist-edits-max' ) . - '

    ' . - $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' ) . - ' - ' . - // Xml::label() cannot be used because 'stub-threshold' contains plain HTML - Xml::tags( 'label', array( 'for' => 'wpStubs' ), wfMsg( 'stub-threshold' ) ) . - ' - ' . - Xml::input( 'wpStubs', 6, $this->mStubs, array( 'id' => 'wpStubs' ) ) . - ' - - ' . - Xml::label( wfMsg( 'tog-underline' ), 'wpOpunderline' ) . - ' - ' . - 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' ) . - ' - ' . - 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( '
    ' ); - - wfRunHooks( 'RenderPreferencesForm', array( $this, $wgOut ) ); - $token = htmlspecialchars( $wgUser->editToken() ); - $skin = $wgUser->getSkin(); - $rtl = $wgContLang->isRTL() ? 'left' : 'right'; - $wgOut->addHTML( " - - - -
    tooltipAndAccesskey('save')." /> -
    + $url = SpecialPage::getTitleFor( 'Preferences' )->getFullURL( 'success' ); - - \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' ) . " " . - Xml::input( 'from', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . + Xml::input( 'prefix', 30, str_replace('_',' ',$from), array( 'id' => 'nsfrom' ) ) . " @@ -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 ? '
    ' : '' ) . - $sk->makeKnownLinkObj( $t, htmlspecialchars( $t->getText() ), false, false ) . + $sk->linkKnown( + $t, + htmlspecialchars( $t->getText() ) + ) . ($s->page_is_redirect ? '
    ' : '' ); } else { $link = '[[' . htmlspecialchars( $s->page_title ) . ']]'; @@ -170,17 +173,26 @@ class SpecialPrefixindex extends SpecialAllpages { $nsForm . ' ' . - $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 .= "" . 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 '
  • ' . wfSpecialList( $link . $stxt, implode( $description_items, ', ' ) ) . $changeProtection . "
  • \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 ) . " \n" . $this->getTypeMenu( $type ) . " \n" . $this->getLevelMenu( $level ) . " \n" . - "
    " . + "
    " . $this->getExpiryCheck( $indefOnly ) . " \n" . $this->getCascadeCheck( $cascadeOnly ) . " \n" . - "
    " . + "
    " . $this->getSizeLimit( $sizetype, $size ) . " \n" . "" . " " . 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 ) . ' ' . + return "" . + Xml::label( wfMsg( 'restriction-level' ) , $this->IdLevel ) . ' ' . Xml::tags( 'select', array( 'id' => $this->IdLevel, 'name' => $this->IdLevel ), - implode( "\n", $options ) ); + implode( "\n", $options ) ) . ""; } } 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 "
    \n" . '
    ' . 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 ); @@ -400,6 +416,14 @@ class SpecialRecentChanges extends SpecialPage { $wgOut->addHTML( $s ); } + /** + * 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 * @@ -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(), '' . htmlspecialchars( $title ) . '', + 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 ) ) . '
    '; + $wgLang->timeanddate( $options['from'], true ), + $wgLang->date( $options['from'], true ), + $wgLang->time( $options['from'], true ) ) . '
    '; } # 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( '
    $1

    ', 'allpagesbadtitle' ); + $wgOut->wrapWikiMsg( "
    \n$1

    ", '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 @@ 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( '' . wfMsgExt( 'removerestrictions-success', 'parseinline', $r->getSubjectText() ) . '' ); - $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( "
    " ); } 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 . - '' . - '' . + "\n" . + "\n" . '' . Xml::submitButton( wfMsg( $submitMsg ) ) . - '' . - '' . + Xml::submitButton( wfMsg( 'resetpass-submit-cancel' ), array( 'name' => 'wpCancel' ) ) . + "\n" . + "\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 .= ''; - $out .= ""; + $out .= "\n"; + $out .= "\t"; if ( $type != 'text' ) $out .= Xml::label( wfMsg( $label ), $name ); else - $out .= wfMsg( $label ); - $out .= ''; - $out .= ""; + $out .= wfMsgHtml( $label ); + $out .= "\n"; + $out .= "\t"; $out .= $field; - $out .= ''; - $out .= ''; + $out .= "\n"; + $out .= ""; } 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 @@ -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 = '' . wfMsgHtml( 'restrictuser-notfound' ) . ''; - $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' ) ) . "
    "; - if( $error ) - $s .= '

    ' . $error . '

    '; - $s .= Xml::hidden( 'title', $wgTitle->getPrefixedDbKey() ); - $form = array( 'restrictuser-user' => Xml::input( 'user', false, $val ) ); - $s .= Xml::buildForm( $form, 'restrictuser-go' ); - $s .= "
    "; - return $s; - } - - public static function existingRestrictions( $restrictions ) { - //TODO: autoload? - require_once( dirname( __FILE__ ) . '/SpecialListUserRestrictions.php' ); - $s = Xml::fieldset( wfMsg( 'restrictuser-existing' ) ) . '
      '; - foreach( $restrictions as $r ) - $s .= UserRestrictionsPager::formatRestriction( $r ); - $s .= "
    "; - 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( "" ); - } - - public static function printSuccessError( $success, $error ) { - global $wgOut; - if ( $error ) - $wgOut->wrapWikiMsg( '$1', $error ); - if ( $success ) - $wgOut->wrapWikiMsg( '$1', $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( "" ); - } - - 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( '

    '.implode($links,' / ').'

    ' ); - } - # 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( "

    " . htmlspecialchars( LogPage::logName( 'delete' ) ) . "

    \n" ); - LogEventsList::showLogExtract( $wgOut, 'delete', $this->page->getPrefixedText() ); - if( $wgUser->isAllowed( 'suppressionlog' ) ){ - $wgOut->addHTML( "

    " . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "

    \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( "

    " . htmlspecialchars( LogPage::logName( 'delete' ) ) . "

    \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( "

    " . htmlspecialchars( LogPage::logName( 'suppress' ) ) . "

    \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( '

    ' . $wgLang->pipeList( $links ) . '

    ' ); } } + /** + * 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' ) ) . + '' + ); + 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,1343 +297,1551 @@ 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; - $UserAllowed = true; - - $count = ($this->deleteKey=='oldid') ? - count($this->revisions) : count($this->archrevs); - $wgOut->addWikiMsg( 'revdelete-selected', $this->page->getPrefixedText(), $count ); - - $bitfields = 0; - $wgOut->addHTML( "
      " ); - - $where = $revObjs = array(); - $dbr = wfGetDB( DB_MASTER ); - - $revisions = 0; - // Live revisions... - if( $this->deleteKey=='oldid' ) { - // Run through and pull all our data in one query - foreach( $this->revisions as $revid ) { - $where[] = intval($revid); - } - $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; - } - $revisions++; - $wgOut->addHTML( $this->historyLine( $revObjs[$timestamp] ) ); - $bitfields |= $revObjs[$timestamp]->mDeleted; - } - } - if( !$revisions ) { - $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); - return; - } - - $wgOut->addHTML( "
    " ); - // 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=='oldid' ) { - foreach( $revObjs as $rev ) - $hidden[] = Xml::hidden( 'oldid[]', $rev->getId() ); - } else { - foreach( $revObjs as $rev ) - $hidden[] = Xml::hidden( 'artimestamp[]', $rev->getTimestamp() ); - } - $special = SpecialPage::getTitleFor( 'Revisiondelete' ); - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $special->getLocalUrl( 'action=submit' ), - 'id' => 'mw-revdel-form-revisions' ) ) . - Xml::openElement( 'fieldset' ) . - xml::element( 'legend', null, wfMsg( 'revdelete-legend' ) ) - ); - - $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) ); - foreach( $items as $item ) { - $wgOut->addHTML( Xml::tags( 'p', null, $item ) ); - } - foreach( $hidden as $item ) { - $wgOut->addHTML( $item ); + protected function getList() { + if ( is_null( $this->list ) ) { + $class = $this->typeInfo['list-class']; + $this->list = new $class( $this, $this->targetObj, $this->ids ); } - $wgOut->addHTML( - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . "\n" - ); + return $this->list; } /** - * This lets a user set restrictions for archived images + * 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. */ - private function showImages() { + protected function showForm() { 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( "
      " ); - - $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; - } - // 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; - } - // Archived files... - } 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; - } - $revisions++; - // Inject history info - $wgOut->addHTML( $this->archivedfileLine( $filesObjs[$fileid] ) ); - $bitfields |= $filesObjs[$fileid]->deleted; - } - } - if( !$revisions ) { - $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); - return; - } - - $wgOut->addHTML( "
    " ); - // 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 ); + if ( $this->typeName == 'logging' ) { + $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->ids) ) ); } 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' ) ) - ); - - $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) ); - foreach( $items as $item ) { - $wgOut->addHTML( "

    $item

    " ); - } - foreach( $hidden as $item ) { - $wgOut->addHTML( $item ); + $wgOut->addWikiMsg( 'revdelete-selected', + $this->targetObj->getPrefixedText(), count( $this->ids ) ); } - $wgOut->addHTML( - Xml::closeElement( 'fieldset' ) . - Xml::closeElement( 'form' ) . "\n" - ); - } - - /** - * This lets a user set restrictions for log items - */ - private function showLogItems() { - global $wgOut, $wgUser, $wgMessageCache, $wgLang; - $UserAllowed = true; - - $wgOut->addWikiMsg( 'logdelete-selected', $wgLang->formatNum( count($this->events) ) ); - - $bitfields = 0; $wgOut->addHTML( "
      " ); - $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); - } - 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; - } - $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 ) { + $where = $revObjs = array(); + + $numRevisions = 0; + // Live revisions... + $list = $this->getList(); + for ( $list->reset(); $list->current(); $list->next() ) { + $item = $list->current(); + if ( !$item->canView() ) { + if( !$this->submitClicked ) { $wgOut->permissionRequired( 'suppressrevision' ); return; } $UserAllowed = false; } - $logItems++; - $wgOut->addHTML( $this->logLine( $logRows[$logid] ) ); - $bitfields |= $logRows[$logid]->log_deleted; + $numRevisions++; + $wgOut->addHTML( $item->getHTML() ); } - if( !$logItems ) { - $wgOut->showErrorPage( 'revdelete-nologid-title', 'revdelete-nologid-text' ); + + if( !$numRevisions ) { + $wgOut->showErrorPage( 'revdelete-nooldid-title', 'revdelete-nooldid-text' ); return; } $wgOut->addHTML( "
    " ); // 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 ) ); - 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' ) ) - ); - - $wgOut->addHTML( $this->buildCheckBoxes( $bitfields ) ); - foreach( $items as $item ) { - $wgOut->addHTML( "

    $item

    " ); - } - foreach( $hidden as $item ) { - $wgOut->addHTML( $item ); + // 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' ) . + "\n" . + '' . + Xml::label( wfMsg( 'revdelete-log' ), 'wpRevDeleteReasonList' ) . + '' . + '' . + Xml::listDropDown( 'wpRevDeleteReasonList', + wfMsgForContent( 'revdelete-reason-dropdown' ), + wfMsgForContent( 'revdelete-reasonotherlist' ), '', 'wpReasonDropDown', 1 + ) . + '' . + "\n" . + '' . + Xml::label( wfMsg( 'revdelete-otherreason' ), 'wpReason' ) . + '' . + '' . + Xml::input( 'wpReason', 60, $this->otherReason, array( 'id' => 'wpReason' ) ) . + '' . + "\n" . + '' . + '' . + Xml::submitButton( wfMsgExt('revdelete-submit','parsemag',$numRevisions), + array( 'name' => 'wpSubmit' ) ) . + '' . + "\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 { + $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 ); } - - private function addUsageText() { + + /** + * Show some introductory text + * FIXME Wikimedia-specific policy 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' ); + } } /** - * @param int $bitfields, aggregate bitfield of all the bitfields - * @returns string HTML + * @return 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 = "$line"; - $html .= $line; + protected function buildCheckBoxes() { + global $wgRequest; + + $html = ''; + // 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 ); + } + 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 = "$innerHTML"; + $line = Xml::tags( 'td', array( 'class' => 'mw-input' ), $innerHTML ); + $html .= "$line\n"; + } + // Otherwise, use tri-state radios + } else { + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= "\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 = ''; + $line .= ''; + $line .= ''; + $label = wfMsgHtml($message); + if( $field == Revision::DELETED_RESTRICTED ) { + $label = "$label"; + } + $line .= ""; + $html .= "$line\n"; + } } + + $html .= '
    '.wfMsgHtml('revdelete-radio-same').''.wfMsgHtml('revdelete-radio-unset').''.wfMsgHtml('revdelete-radio-set').'
    ' . Xml::radio( $name, -1, $selected == -1 ) . '' . Xml::radio( $name, 0, $selected == 0 ) . '' . Xml::radio( $name, 1, $selected == 1 ) . '$label
    '; return $html; } /** - * @param Revision $rev - * @returns string + * UI entry point for form submission. + * @param $request WebRequest */ - private function historyLine( $rev ) { - global $wgLang, $wgUser; - - $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 + 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; + } + $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; + } + # 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 { - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $target = $this->page->getPrefixedText(); - $revlink = $this->skin->makeLinkObj( $undelete, $date, - "target=$target×tamp=" . $rev->getTimestamp() ); - $difflink = '(' . $this->skin->makeKnownLinkObj( $undelete, wfMsgHtml('diff'), - "target=$target&diff=prev×tamp=" . $rev->getTimestamp() ) . ')'; - } - // Check permissions; items may be "suppressed" - if( $rev->isDeleted(Revision::DELETED_TEXT) ) { - $revlink = ''.$revlink.''; - $del = ' ' . wfMsgHtml( 'deletedrev' ) . ''; - if( !$rev->userCan(Revision::DELETED_TEXT) ) { - $revlink = ''.$date.''; - $difflink = '(' . wfMsgHtml('diff') . ')'; - } + $this->failure( $status ); } - $userlink = $this->skin->revUserLink( $rev ); - $comment = $this->skin->revComment( $rev ); - - return "
  • $difflink $revlink $userlink $comment{$del}
  • "; + return false; } /** - * @param File $file - * @returns string + * Report that the submit operation succeeded */ - private function fileLine( $file ) { - global $wgLang, $wgTitle; - - $target = $this->page->getPrefixedText(); - $date = $wgLang->timeanddate( $file->getTimestamp(), true ); - - $del = ''; - # Hidden files... - if( $file->isDeleted(File::DELETED_FILE) ) { - $del = ' ' . wfMsgHtml( 'deletedrev' ) . ''; - if( !$file->userCan(File::DELETED_FILE) ) { - $pageLink = $date; - } else { - $pageLink = $this->skin->makeKnownLinkObj( $wgTitle, $date, - "target=$target&file=$file->sha1.".$file->getExtension() ); - } - $pageLink = '' . $pageLink . ''; - # Regular files... - } else { - $url = $file->getUrlRel(); - $pageLink = "{$date}"; - } - - $data = wfMsg( 'widthheight', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ) ) . - ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')'; - $data = htmlspecialchars( $data ); - - return "
  • $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del
  • "; + protected function success() { + global $wgOut; + $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); + $wgOut->wrapWikiMsg( '$1', $this->typeInfo['success'] ); + $this->list->reloadFromMaster(); + $this->showForm(); } /** - * @param ArchivedFile $file - * @returns string + * Report that the submit operation failed */ - private function archivedfileLine( $file ) { - global $wgLang; - - $target = $this->page->getPrefixedText(); - $date = $wgLang->timeanddate( $file->getTimestamp(), true ); - - $undelete = SpecialPage::getTitleFor( 'Undelete' ); - $pageLink = $this->skin->makeKnownLinkObj( $undelete, $date, "target=$target&file={$file->getKey()}" ); - - $del = ''; - if( $file->isDeleted(File::DELETED_FILE) ) { - $del = ' ' . wfMsgHtml( 'deletedrev' ) . ''; - } - - $data = wfMsg( 'widthheight', - $wgLang->formatNum( $file->getWidth() ), - $wgLang->formatNum( $file->getHeight() ) ) . - ' (' . wfMsgExt( 'nbytes', 'parsemag', $wgLang->formatNum( $file->getSize() ) ) . ')'; - $data = htmlspecialchars( $data ); - - return "
  • $pageLink ".$this->fileUserTools( $file )." $data ".$this->fileComment( $file )."$del
  • "; + protected function failure( $status ) { + global $wgOut; + $wgOut->setPagetitle( wfMsg( 'actionfailed' ) ); + $wgOut->addWikiText( $status->getWikiText( $this->typeInfo['failure'] ) ); + $this->showForm(); } /** - * @param Array $row row - * @returns string + * Put together an array that contains -1, 0, or the *_deleted const for each bit + * @param $request WebRequest + * @return array */ - private function logLine( $row ) { - global $wgLang; - - $date = $wgLang->timeanddate( $row->log_timestamp ); - $paramArray = LogPage::extractParams( $row->log_params ); - $title = Title::makeTitle( $row->log_namespace, $row->log_title ); - - $logtitle = SpecialPage::getTitleFor( 'Log' ); - $loglink = $this->skin->makeKnownLinkObj( $logtitle, wfMsgHtml( 'log' ), - wfArrayToCGI( array( 'page' => $title->getPrefixedUrl() ) ) ); - // Action text - if( !LogEventsList::userCan($row,LogPage::DELETED_ACTION) ) { - $action = '' . wfMsgHtml('rev-deleted-event') . ''; - } else { - $action = LogPage::actionText( $row->log_type, $row->log_action, $title, - $this->skin, $paramArray, true, true ); - if( $row->log_deleted & LogPage::DELETED_ACTION ) - $action = '' . $action . ''; - } - // User links - $userLink = $this->skin->userLink( $row->log_user, User::WhoIs($row->log_user) ); - if( LogEventsList::isDeleted($row,LogPage::DELETED_USER) ) { - $userLink = '' . $userLink . ''; + 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; } - // Comment - $comment = $wgLang->getDirMark() . $this->skin->commentBlock( $row->log_comment ); - if( LogEventsList::isDeleted($row,LogPage::DELETED_COMMENT) ) { - $comment = '' . $comment . ''; + if( !isset($bitfield[Revision::DELETED_RESTRICTED]) ) { + $bitfield[Revision::DELETED_RESTRICTED] = 0; } - return "
  • ($loglink) $date $userLink $action $comment
  • "; + return $bitfield; } - + /** - * Generate a user tool link cluster if the current user is allowed to view it - * @param ArchivedFile $file - * @return string HTML + * Put together a rev_deleted bitfield + * @param $bitPars array extractBitParams() params + * @param $oldfield int current bitfield + * @return array */ - private function fileUserTools( $file ) { - if( $file->userCan( Revision::DELETED_USER ) ) { - $link = $this->skin->userLink( $file->user, $file->user_text ) . - $this->skin->userToolLinks( $file->user, $file->user_text ); - } else { - $link = wfMsgHtml( 'rev-deleted-user' ); - } - if( $file->isDeleted( Revision::DELETED_USER ) ) { - return '' . $link . ''; + 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 + } } - return $link; + return $newBits; } /** - * Wrap and format the given file's comment block, if the current - * user is allowed to view it. - * - * @param ArchivedFile $file - * @return string HTML + * Do the write operations. Simple wrapper for RevDel_*List::setVisibility(). */ - 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 "$block"; - } - return $block; + protected function save( $bitfield, $reason, $title ) { + return $this->getList()->setVisibility( + array( 'value' => $bitfield, 'comment' => $reason ) + ); } +} +/** + * Temporary b/c interface, collection of static functions. + * @ingroup SpecialPage + */ +class RevisionDeleter { /** - * @param WebRequest $request + * 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. */ - 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(); + protected static function checkItem( $desc, $field, $diff, $new, &$arr ) { + if( $diff & $field ) { + $arr[ ( $new & $field ) ? 0 : 1 ][] = $desc; } } - private function success() { - global $wgOut; - - $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) ); - - $wrap = '$1'; - - 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(); + /** + * 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' ); } + return $ret; } /** - * Put together a rev_deleted bitfield from the submitted checkboxes - * @param WebRequest $request - * @return int + * 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 */ - private function extractBitfield( $request ) { - $bitfield = 0; - foreach( $this->checks as $item ) { - list( /* message */ , $name, $field ) = $item; - if( $request->getCheck( $name ) ) { - $bitfield |= $field; - } + 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] ) ); } - return $bitfield; - } + 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) ); - 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 ); + } + + // 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; } } } /** - * Implements the actions for Revision Deletion. - * @ingroup SpecialPage + * Abstract base class for a list of deletable items */ -class RevisionDeleter { - function __construct( $db ) { - $this->dbw = $db; +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 $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 $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 internal type name of this list. Equal to the table name. */ - function setRevVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + public function getType() { + return $this->type; + } - $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 DB field name associated with the ID list + */ + public function getIdField() { + return $this->idField; + } - $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 DB field name storing timestamps + */ + public function getTimestampField() { + return $this->dateField; + } - return $success; + /** + * Get the DB field name storing user ids + */ + public function getAuthorIdField() { + return $this->authorIdField; } - /** - * @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 DB field name storing user names */ - function setArchiveVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + 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']; - $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; + $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 { + $opType = 'modify'; } - // Which revisions did we change anything about? - if( $revObjs[$timestamp]->mDeleted != $bitfield ) { - $Id_set[]=$timestamp; - $count++; - $this->updateArchive( $revObjs[$timestamp], $title, $bitfield ); + if ( $item->isHideCurrentOp( $newBits ) ) { + // Cannot hide current version text + $status->error( 'revdelete-hide-current', $item->formatDate(), $item->formatTime() ); + $status->failCount++; + continue; } - } - // For logging, maintain a count of revisions - if( $count > 0 ) { - $this->updateLog( $title, $count, $bitfield, $revObjs[$timestamp]->mDeleted, - $comment, $title, 'artimestamp', $Id_set ); - } - // Where all revs allowed to be set? - if( !$userAllowedAll ) { - $wgOut->permissionRequired( 'suppressrevision' ); - return false; - } - - return $success; - } - - /** - * @param $title, the page these events apply to - * @param array $items list of revision ID numbers - * @param int $bitfield new rev_deleted value - * @param string $comment Comment for log records - */ - function setOldImgVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; - - $userAllowedAll = $success = true; - $count = 0; - $set = array(); - // Run through and pull all our data in one query - foreach( $items as $timestamp ) { - $where[] = $timestamp.'!'.$title->getDBKey(); - } - $result = $this->dbw->select( 'oldimage', '*', - 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; + 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; } - - $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; + // 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(); } - $this->dbw->commit(); + } else { + $status->error( 'revdelete-concurrent-change', $item->formatDate(), $item->formatTime() ); + $status->failCount++; } } - // 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; + // Handle missing revisions + foreach ( $missing as $id => $unused ) { + $status->error( 'revdelete-modify-missing', $id ); + $status->failCount++; } - return $success; - } - - /** - * @param $title, the page these events apply to - * @param array $items list of revision ID numbers - * @param int $bitfield new rev_deleted value - * @param string $comment Comment for log records - */ - function setArchFileVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; + if ( $status->successCount == 0 ) { + $status->ok = false; + $dbw->rollback(); + return $status; + } - $userAllowedAll = $success = true; - $count = 0; - $Id_set = array(); + // Save success count + $successCount = $status->successCount; - // Run through and pull all our data in one query - foreach( $items as $id ) { - $where[] = intval($id); + // 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; } - $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++; - $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; - } + // Log it + $this->updateLog( array( + 'title' => $this->title, + 'count' => $successCount, + 'newBits' => $newBits, + 'oldBits' => $oldBits, + 'comment' => $comment, + 'ids' => $idsForLog, + 'authorIds' => $authorIds, + 'authorIPs' => $authorIPs + ) ); + $dbw->commit(); - return $success; + // Clear caches + $status->merge( $this->doPostCommitUpdates() ); + return $status; } /** - * @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 + * Reload the list data from the master DB. This can be done after setVisibility() + * to allow $item->getHTML() to show the new data. */ - function setEventVisibility( $title, $items, $bitfield, $comment ) { - global $wgOut; - - $userAllowedAll = $success = true; - $count = 0; - $log_Ids = array(); + function reloadFromMaster() { + $dbw = wfGetDB( DB_MASTER ); + $this->res = $this->doQuery( $dbw ); + } - // Run through and pull all our data in one query - foreach( $items as $logid ) { - $where[] = intval($logid); + /** + * 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!" ); } - list($log,$logtype) = explode( '/',$title->getDBKey(), 2 ); - $result = $this->dbw->select( 'logging', '*', - 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++; + // 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 ); + } - $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; - } + /** + * Get the log action for this list type + */ + public function getLogAction() { + return 'revision'; + } - return $success; + /** + * 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']}" + ); } /** - * 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 + * Initialise the current iteration pointer */ - function makeOldImagePrivate( $oimage ) { - $transaction = new FSTransaction(); - if( !FileStore::lock() ) { - wfDebug( __METHOD__.": failed to acquire file store lock, aborting\n" ); - return false; + protected function initCurrent() { + $row = $this->res->current(); + if ( $row ) { + $this->current = $this->newItem( $row ); + } else { + $this->current = 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 - } + } + + /** + * Start iteration. This must be called before current() or next(). + * @return First list item + */ + public function reset() { + if ( !$this->res ) { + $this->res = $this->doQuery( wfGetDB( DB_SLAVE ) ); } else { - wfDebug( __METHOD__." deleting already-missing '$oldpath'; moving on to database\n" ); - $group = null; - $key = ''; - $transaction = new FSTransaction(); // empty + $this->res->rewind(); } + $this->initCurrent(); + return $this->current; + } - 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; + /** + * Get the current list item, or false if we are at the end + */ + public function current() { + return $this->current; + } + + /** + * Move the iteration pointer to the next list item, and return it. + */ + 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(); } + } - wfDebug( __METHOD__.": set db items, applying file transactions\n" ); - $transaction->commit(); - FileStore::unlock(); + /** + * Clear any data structures needed for doPreCommitUpdates() and doPostCommitUpdates() + * STUB + */ + public function clearFileOps() { + } - $m = explode('!',$oimage->archive_name,2); - $timestamp = $m[0]; + /** + * A hook for setVisibility(): do batch updates pre-commit. + * STUB + * @return Status + */ + public function doPreCommitUpdates() { + return Status::newGood(); + } - return $timestamp; + /** + * A hook for setVisibility(): do any necessary updates post-commit. + * STUB + * @return Status + */ + public function doPostCommitUpdates() { + 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 + * Create an item object from a DB result row + * @param $row stdclass */ - function makeOldImagePublic( $oimage ) { - $transaction = new FSTransaction(); - if( !FileStore::lock() ) { - wfDebug( __METHOD__." could not acquire filestore lock\n" ); - return false; - } + abstract public function newItem( $row ); - $store = FileStore::get( 'deleted' ); - if( !$store ) { - wfDebug( __METHOD__.": skipping row with no file.\n" ); - return false; - } + /** + * Do the DB query to iterate through the objects. + * @param $db Database object to use for the query + */ + abstract public function doQuery( $db ); - $key = $oimage->sha1.'.'.$oimage->getExtension(); - $destDir = $oimage->getArchivePath(); - if( !is_dir( $destDir ) ) { - wfMkdirParents( $destDir ); - } - $destPath = $destDir . DIRECTORY_SEPARATOR . $oimage->archive_name; - // Check if any other stored revisions use this file; - // if so, we shouldn't remove the file from the hidden - // archives so they will still work. Check hidden files first. - $useCount = $this->dbw->selectField( 'oldimage', '1', - array( 'oi_sha1' => $oimage->sha1, - 'oi_deleted & '.File::DELETED_FILE => File::DELETED_FILE ), - __METHOD__, array( 'FOR UPDATE' ) ); - // Check the rest of the deleted archives too. - // (these are the ones that don't show in the image history) - if( !$useCount ) { - $useCount = $this->dbw->selectField( 'filearchive', '1', - array( 'fa_storage_group' => 'deleted', 'fa_storage_key' => $key ), - __METHOD__, array( 'FOR UPDATE' ) ); - } - - if( $useCount == 0 ) { - wfDebug( __METHOD__.": nothing else using {$oimage->sha1}, will deleting after\n" ); - $flags = FileStore::DELETE_ORIGINAL; - } else { - $flags = 0; - } - $transaction->add( $store->export( $key, $destPath, $flags ) ); + /** + * Get the integer value of the flag used for suppression + */ + abstract public function getSuppressBit(); +} - wfDebug( __METHOD__.": set db items, applying file transactions\n" ); - $transaction->commit(); - FileStore::unlock(); +/** + * Abstract base class for deletable items + */ +abstract class RevDel_Item { + /** The parent SpecialPage */ + var $special; - $m = explode('!',$oimage->archive_name,2); - $timestamp = $m[0]; + /** The parent RevDel_List */ + var $list; - return $timestamp; + /** 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; } /** - * Update the revision's rev_deleted field - * @param Revision $rev - * @param int $bitfield new rev_deleted bitfield value + * Get the ID, as it would appear in the ids URL parameter */ - function updateRevision( $rev, $bitfield ) { - $this->dbw->update( 'revision', - array( 'rev_deleted' => $bitfield ), - array( 'rev_id' => $rev->getId(), - 'rev_page' => $rev->getPage() ), - __METHOD__ ); + public function getId() { + $field = $this->list->getIdField(); + return $this->row->$field; } /** - * Update the revision's rev_deleted field - * @param Revision $rev - * @param Title $title - * @param int $bitfield new rev_deleted bitfield value + * Get the date, formatted with $wgLang */ - 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 formatDate() { + global $wgLang; + return $wgLang->date( $this->getTimestamp() ); } /** - * Update the images's oi_deleted field - * @param File $file - * @param int $bitfield new rev_deleted bitfield value + * Get the time, formatted with $wgLang */ - 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 formatTime() { + global $wgLang; + return $wgLang->time( $this->getTimestamp() ); } /** - * Update the images's fa_deleted field - * @param ArchivedFile $file - * @param int $bitfield new rev_deleted bitfield value + * Get the timestamp in MW 14-char form */ - function updateArchFiles( $file, $bitfield ) { - $this->dbw->update( 'filearchive', - array( 'fa_deleted' => $bitfield ), - array( 'fa_id' => $file->getId() ), - __METHOD__ ); + 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 ); } /** - * Update the logging log_deleted field - * @param Row $row - * @param int $bitfield new rev_deleted bitfield value + * Get the author user name + */ + public function getAuthorName() { + $field = $this->list->getAuthorNameField(); + return strval( $this->row->$field ); + } + + /** + * Returns true if the item is "current", and the operation to set the given + * bits can't be executed for that reason + * STUB */ - function updateLogs( $row, $bitfield ) { - $this->dbw->update( 'logging', - array( 'log_deleted' => $bitfield ), - array( 'log_id' => $row->log_id ), - __METHOD__ ); + public function isHideCurrentOp( $newBits ) { + return false; } /** - * Update the revision's recentchanges record if fields have been hidden - * @param Revision $rev - * @param int $bitfield new rev_deleted bitfield value + * Returns true if the current user can view the item */ - 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__ ); - } + abstract public function canView(); + + /** + * Returns true if the current user can view the item text/file + */ + abstract public function canViewContent(); /** - * Update the revision's recentchanges record if fields have been hidden - * @param Row $row - * @param int $bitfield new rev_deleted bitfield value + * Get the current deletion 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__ ); - } + abstract public function getBits(); /** - * 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 HTML of the list item. Should be include
  • tags. + * This is used to show the list in HTML form, by the special page. */ - function updatePage( $title ) { - $title->invalidateCache(); - $title->purgeSquid(); - $title->touchLinks(); - // Extensions that require referencing previous revisions may need this - wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$title ) ); - } + abstract public function getHTML(); /** - * Checks for a change in the bitfield for a certain option and updates the - * provided array accordingly. + * Set the visibility of the item. This should do any necessary DB queries. * - * @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. + * 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 checkItem ( $desc, $field, $diff, $new, &$arr ) { - if ( $diff & $field ) { - $arr [ ( $new & $field ) ? 0 : 1 ][] = $desc; + abstract public function setBits( $newBits ); +} + +/** + * 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( + 'rev_page' => $this->title->getArticleID(), + 'rev_id' => $ids, + 'rev_page = page_id' + ), + __METHOD__, + array( 'ORDER BY' => 'rev_id DESC' ) + ); + } + + public function newItem( $row ) { + return new RevDel_RevisionItem( $this, $row ); + } + + public function getCurrent() { + if ( is_null( $this->currentRevId ) ) { + $dbw = wfGetDB( DB_MASTER ); + $this->currentRevId = $dbw->selectField( + 'page', 'page_latest', $this->title->pageCond(), __METHOD__ ); } + return $this->currentRevId; } - /** - * Gets an array describing the changes made to the visibilit of the revision. - * If the resulting array is $arr, then $arr[0] will contain an array of strings - * describing the items that were hidden, $arr[2] will contain an array of strings - * describing the items that were unhidden, and $arr[3] will contain an array with - * a single string, which can be one of "applied restrictions to sysops", - * "removed restrictions from sysops", or null. - * - * @param int $n The new bitfield. - * @param int $o The old bitfield. - * @return An array as described above. - */ - function getChanges ( $n, $o ) { - $diff = $n ^ $o; - $ret = array ( 0 => array(), 1 => array(), 2 => array() ); + public function getSuppressBit() { + return Revision::DELETED_RESTRICTED; + } - $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 doPreCommitUpdates() { + $this->title->invalidateCache(); + return Status::newGood(); + } - // 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' ); + public function doPostCommitUpdates() { + $this->title->purgeSquid(); + // Extensions that require referencing previous revisions may need this + wfRunHooks( 'ArticleRevisionVisiblitySet', array( &$this->title ) ); + return Status::newGood(); + } +} + +/** + * Item class for a revision table row + */ +class RevDel_RevisionItem extends RevDel_Item { + var $revision; + + public function __construct( $list, $row ) { + parent::__construct( $list, $row ); + $this->revision = new Revision( $row ); + } + + public function canView() { + return $this->revision->userCan( Revision::DELETED_RESTRICTED ); + } + + public function canViewContent() { + return $this->revision->userCan( Revision::DELETED_TEXT ); + } + + public function getBits() { + return $this->revision->mDeleted; + } + + 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; } + // Update recentchanges table + $dbw->update( 'recentchanges', + array( + 'rc_deleted' => $bits, + 'rc_patrolled' => 1 + ), + array( + 'rc_this_oldid' => $this->revision->getId(), // condition + // non-unique timestamp index + 'rc_timestamp' => $dbw->timestamp( $this->revision->getTimestamp() ), + ), + __METHOD__ + ); + return true; + } - return $ret; + public function isDeleted() { + return $this->revision->isDeleted( Revision::DELETED_TEXT ); + } + + public function isHideCurrentOp( $newBits ) { + return ( $newBits & Revision::DELETED_TEXT ) + && $this->list->getCurrent() == $this->getId(); } /** - * 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 + * Get the HTML link to the revision text. + * Overridden by RevDel_ArchiveItem. */ - function getLogMessage ( $count, $nbitfield, $obitfield, $comment, $isForLog = false ) { - global $wgContLang; + 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 + ) + ); + } - $s = ''; - $changes = $this->getChanges( $nbitfield, $obitfield ); + /** + * Get the HTML link to the diff. + * Overridden by RevDel_ArchiveItem + */ + protected function getDiffLink() { + if ( $this->isDeleted() && !$this->canViewContent() ) { + return wfMsgHtml('diff'); + } else { + return + $this->special->skin->link( + $this->list->title, + wfMsgHtml('diff'), + array(), + array( + 'diff' => $this->revision->getId(), + 'oldid' => 'prev', + 'unhide' => 1 + ), + array( + 'known', + 'noclasses' + ) + ); + } + } - if ( count ( $changes[0] ) ) { - $s .= wfMsgForContent ( 'revdelete-hid', implode ( ', ', $changes[0] ) ); + 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 = "$revlink"; } + return "
  • ($difflink) $revlink $userlink $comment
  • "; + } +} - if ( count ( $changes[1] ) ) { - if ($s) $s .= '; '; +/** + * 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' ) + ); + } - $s .= wfMsgForContent ( 'revdelete-unhid', implode ( ', ', $changes[1] ) ); + public function newItem( $row ) { + return new RevDel_ArchiveItem( $this, $row ); + } + + public function doPreCommitUpdates() { + return Status::newGood(); + } + + public function doPostCommitUpdates() { + return Status::newGood(); + } +} + +/** + * 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() + ) ); + } - if ( count ( $changes[2] )) { - if ($s) - $s .= ' (' . $changes[2][0] . ')'; - else - $s = $changes[2][0]; + protected function getDiffLink() { + if ( $this->isDeleted() && !$this->canViewContent() ) { + return wfMsgHtml( 'diff' ); } + $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() + ) ); + } +} - $msg = $isForLog ? 'logdelete-log-message' : 'revdelete-log-message'; - $ret = wfMsgExt ( $msg, array( 'parsemag', 'content' ), - $s, $wgContLang->formatNum( $count ) ); +/** + * 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' ) + ); + } - if ( $comment ) - $ret .= ": $comment"; + public function newItem( $row ) { + return new RevDel_FileItem( $this, $row ); + } - return $ret; + public function clearFileOps() { + $this->deleteBatch = array(); + $this->storeBatch = array(); + $this->cleanupBatch = array(); + } + + 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; + } + + public function doPostCommitUpdates() { + $file = wfLocalFile( $this->title ); + $file->purgeCache(); + $file->purgeDescription(); + return Status::newGood(); + } + + public function getSuppressBit() { + return File::DELETED_RESTRICTED; + } +} + +/** + * 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 ); + } + + public function getId() { + $parts = explode( '!', $this->row->oi_archive_name ); + return $parts[0]; + } + + public function canView() { + return $this->file->userCan( File::DELETED_RESTRICTED ); + } + + public function canViewContent() { + return $this->file->userCan( File::DELETED_FILE ); + } + + 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 ); } /** - * 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 + * Get the link to the file. + * Overridden by RevDel_ArchivedFileItem. */ - 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 ); + 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 '' . $link . ''; + } else { + # Regular files... + $url = $this->file->getUrl(); + return Xml::element( 'a', array( 'href' => $this->file->getUrl() ), $date ); + } + } + /** + * Generate a user tool link cluster if the current user is allowed to view it + * @return string HTML + */ + 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 '' . $link . ''; + } + return $link; + } + + /** + * Wrap and format the file's comment block, if the current + * user is allowed to view it. + * + * @return string HTML + */ + 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 "$block"; + } + return $block; + } + + 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 '
  • ' . $this->getLink() . ' ' . $this->getUserTools() . ' ' . + $data . ' ' . $this->getComment(). '
  • '; + } +} + +/** + * 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' ) + ); + } + + public function newItem( $row ) { + return new RevDel_ArchivedFileItem( $this, $row ); + } +} + +/** + * 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 ); + } + + public function getId() { + return $this->row->fa_id; + } - $reason = $this->getLogMessage ( $count, $nbitfield, $obitfield, $comment, $param == 'logid' ); + 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(); + } + + 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 = '' . $link . ''; + } + return $link; + } +} + +/** + * 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' ) + ); + } + + public function newItem( $row ) { + return new RevDel_LogItem( $this, $row ); + } + + public function getSuppressBit() { + return Revision::DELETED_RESTRICTED; + } + + public function getLogAction() { + return 'event'; + } + + public function getLogParams( $params ) { + return array( + implode( ',', $params['ids'] ), + "ofield={$params['oldBits']}", + "nfield={$params['newBits']}" + ); + } +} + +/** + * 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; + } + + 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; + + $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 = '' . wfMsgHtml('rev-deleted-event') . ''; } 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 = '' . $action . ''; } + // 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 = '' . $userLink . ''; + } + // Comment + $comment = $wgLang->getDirMark() . $this->special->skin->commentBlock( $this->row->log_comment ); + if( LogEventsList::isDeleted($this->row,LogPage::DELETED_COMMENT) ) { + $comment = '' . $comment . ''; + } + return "
  • ($loglink) $date $userLink $action $comment
  • "; } } 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 = '
    '.wfMsg('search-suggest',$suggestLink).'
    '; } - - // 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( '' ); // 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( "

    {$top}

    \n" ); + $wgOut->addHTML( $this->formHeader($term, $num, $totalRes)); + if( $this->searchAdvanced ) { + $wgOut->addHTML( $this->powerSearchBox( $term ) ); } + + $wgOut->addHtml( Xml::closeElement( 'form' ) ); + $wgOut->addHtml( "
    " ); // 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( "

    {$prevnext}

    \n" ); + //$wgOut->addHTML( "

    {$prevnext}

    \n" ); wfRunHooks( 'SpecialSearchResults', array( $term, &$titleMatches, &$textMatches ) ); } else { wfRunHooks( 'SpecialSearchNoResults', array( $term ) ); - } + } - $wgOut->addHtml( "
    " ); 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( "
    " ); - 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( "

    {$prevnext}

    \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( '

    ' ); + } + } + /** * */ @@ -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 = "" - .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - .""; + + if( !is_null($redirectTitle) ) { + if( $redirectText == '' ) + $redirectText = null; + + $redirect = "" . + wfMsg( + 'search-redirect', + $this->sk->linkKnown( + $redirectTitle, + $redirectText + ) + ) . + ""; + } + $section = ''; - if( !is_null($sectionTitle) ) - $section = "" - .wfMsg('search-section', $this->sk->makeKnownLinkObj( $sectionTitle, $sectionText)) - .""; + + + if( !is_null($sectionTitle) ) { + if( $sectionText == '' ) + $sectionText = null; + + $section = "" . + wfMsg( + 'search-section', $this->sk->linkKnown( + $sectionTitle, + $sectionText + ) + ) . + ""; + } // format text extract $extract = "
    ".$result->getTextSnippet($terms)."
    "; - + // 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 = "
    ". - wfMsg('search-interwiki-caption')."
    \n"; + wfMsg('search-interwiki-caption')."
    \n"; $off = $this->offset + 1; $out .= "
      \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 .= "
    \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 "\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 = "" - .wfMsg('search-redirect',$this->sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - .""; + if( !is_null($redirectTitle) ) { + if( $redirectText == '' ) + $redirectText = null; + + $redirect = "" . + wfMsg( + 'search-redirect', + $this->sk->linkKnown( + $redirectTitle, + $redirectText + ) + ) . + ""; + } $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 .= "
    {$searchLink}{$caption}
    \n
      "; } - $out .= "
    • {$link} {$redirect}
    • \n"; + $out .= "
    • {$link} {$redirect}
    • \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 = "

      ". $redirect . " " . $redirectLabel ."

      "; + // 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" . - "

      " . - wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . - "

      \n" . - '\n". - $tables . - "
      \n". - $redirectText ."\n". - "
      ". - $searchField . - " " . - Xml::hidden( 'fulltext', 'Advanced search' ) . "\n" . - $searchButton . - "
      ". - ""; - $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 ""; + "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 ""; + foreach( $profiles as $key => &$data ) { + sort($data['namespaces']); + } + + return $profiles; } - protected function formHeader( $term ) { - global $wgContLang, $wgCanonicalNamespaceNames, $wgLang; - - $sep = '   '; - $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' ) ) . "
      \n"; - $out .= $this->namespaceTables( $active, 1 )."
      \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 .= "\n" . $rows[$j] . ""; - } - $tables .= Xml::closeElement( 'table' ) . "\n"; - } - return $tables; - } -} + /** Check if query starts with all: prefix */ + protected function startsWithAll( $term ) { -/** - * 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 ) ); + $allkeyword = wfMsgForContent('searchall'); + + $p = explode( ':', $term ); + if( count( $p ) > 1 ) { + return $p[0] == $allkeyword; } - - $this->showResults( $term, $extra ); + return false; } +} - /** - * @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('
      '.wfMsg('search-suggest',$suggestLink).'
      '); - } - - $wgOut->addHTML( $extra ); - - $wgOut->wrapWikiMsg( "
      \n$1
      ", '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( "

      {$top}

      \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( "

      {$prevnext}

      \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("
      "); - } 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( "

      {$prevnext}

      \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\n"; - - - $off = $this->offset + 1; - $out .= "
        \n"; - - while( $result = $matches->next() ) { - $out .= $this->showHit( $result, $terms ); - } - $out .= "
      \n"; - - // convert the whole thing to desired language variant - global $wgContLang; - $out = $wgContLang->convert( $out ); - wfProfileOut( __METHOD__ ); - return $out; - } - - /** - * Format a single hit result - * @param SearchResult $result - * @param array $terms terms to highlight - */ - function showHit( $result, $terms ) { - wfProfileIn( __METHOD__ ); - global $wgUser, $wgContLang, $wgLang; - - if( $result->isBrokenTitle() ) { - wfProfileOut( __METHOD__ ); - return "\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 "
    • {$link}
    • \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 "\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 = "" - .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - .""; - $section = ''; - if( !is_null($sectionTitle) ) - $section = "" - .wfMsg('search-section', $sk->makeKnownLinkObj( $sectionTitle, $sectionText)) - .""; - - // format text extract - $extract = "
      ".$result->getTextSnippet($terms)."
      "; - - // 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 "
    • " . - '' . - '' . - '' . - '' . - '' . - '
      ' . - $thumb->toHtml( array( 'desc-link' => true ) ) . - '' . - $link . - $extract . - "
      {$score}{$desc} - {$date}{$related}
      " . - '
      ' . - "
    • \n"; - } - } - } - - wfProfileOut( __METHOD__ ); - return "
    • {$link} {$redirect} {$section} {$extract}\n" . - "
      {$score}{$size} - {$date}{$related}
      " . - "
    • \n"; - - } - - /** - * Show results from other wikis - * - * @param SearchResultSet $matches - */ - function showInterwiki( &$matches, $query ) { - wfProfileIn( __METHOD__ ); - - global $wgContLang; - $terms = $wgContLang->convertForSearchResult( $matches->termMatches() ); - - $out = "
      ".wfMsg('search-interwiki-caption')."
      \n"; - $off = $this->offset + 1; - $out .= "
        \n"; - - // work out custom project captions - $customCaptions = array(); - $customLines = explode("\n",wfMsg('search-interwiki-custom')); // format per line : - 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 .= "
      \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 "\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 = "" - .wfMsg('search-redirect',$sk->makeKnownLinkObj( $redirectTitle, $redirectText)) - .""; - - $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 .= "
    {$searchLink}{$caption}
    \n
      "; - } - - $out .= "
    • {$link} {$redirect}
    • \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 .= "\n" . $rows[$j] . ""; - } - $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' ), - "

      " . - wfMsgExt( 'powersearch-ns', array( 'parseinline' ) ) . - "

      \n" . - $tables . - "
      \n" . - "

      " . - $redirect . " " . $redirectLabel . - "

      \n" . - wfMsgExt( 'powersearch-field', array( 'parseinline' ) ) . - " " . - $searchField . - " " . - $searchHiddens . - $searchButton ) . - ""; - - return $out; - } - - function powerSearchFocus() { - global $wgJsMimeType; - return ""; - } - - 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' ); - } - } - $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; - } -} 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 ''; } - $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( "

      $1

      \n", "specialpages-group-$group" ); + $wgOut->wrapWikiMsg( "

      $1

      \n", "specialpages-group-$group" ); $wgOut->addHTML( "" ); $wgOut->addHTML( "
        \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( "
      • {$link}
      • \n" ); + $wgOut->addHTML( "
      • {$link}
      • \n" ); } else { $wgOut->addHTML( "
      • {$link}
      • \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( "
        \n$1
        \n", array( 'filedelete-maintenance' ) ); + return; + } return $this->undelete(); } if( $this->mInvert && $this->mAction == "submit" ) { @@ -707,8 +706,12 @@ class UndeleteForm { $wgOut->addHTML( "
          \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( "\n", 'rev-deleted-text-view' ); - $wgOut->addHTML( '
          ' ); + $wgOut->addHTML( '
          ' ); // 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( '
          ' ); } } 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( '

          ' . wfMsgHtml( 'undelete-revision', $link, $time, $user, $d, $t ) . '

          ' ); + if( $this->mPreview ) { + $openDiv = '
          '; + } else { + $openDiv = '
          '; + } + + // 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 ) . '
          ' ); wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ); if( $this->mPreview ) { - $wgOut->addHTML( "
          \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() ) . "
      " . - "\n" ); - + "\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() . - '×tamp=' . - 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 '
      ' . - $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 + ) . '
      ' . '
      ' . - $sk->revUserTools( $rev ) . '
      ' . + $sk->revUserTools( $rev ) . '
      ' . '
      ' . '
      ' . - $sk->revComment( $rev ) . '
      ' . + $sk->revComment( $rev ) . $del . '
      ' . '
      '; } @@ -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( "
      \n$1
      \n", array ( 'undeletepagetitle', $this->mTargetObj->getPrefixedText() ) ); $archive = new PageArchive( $this->mTargetObj ); /* @@ -951,12 +1004,14 @@ class UndeleteForm { return; } */ + $wgOut->addHTML( '
      ' ); if ( $this->mAllowed ) { $wgOut->addWikiMsg( "undeletehistory" ); $wgOut->addWikiMsg( "undeleterevdel" ); } else { $wgOut->addWikiMsg( "undeletehistorynoadmin" ); } + $wgOut->addHTML( '
      ' ); # 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' ) ) . " - " . + " . wfMsgWikiHtml( 'undeleteextrahelp' ) . " @@ -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() . "×tamp=$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 "
    • $checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment
    • "; } @@ -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 "
    • $checkBox $revdlink $pageLink . . $userLink $data $comment
    • \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 '' . $wgLang->timeanddate( $ts, true ) . ''; + return '' . $time . ''; } else { - $link = $sk->makeKnownLinkObj( $titleObj, $wgLang->timeanddate( $ts, true ), - "target=".$this->mTargetObj->getPrefixedUrl()."×tamp=$ts" ); + $link = $sk->linkKnown( + $titleObj, + $time, + array(), + array( + 'target' => $this->mTargetObj->getPrefixedText(), + 'timestamp' => $ts + ) + ); if( $rev->isDeleted(Revision::DELETED_TEXT) ) $link = '' . $link . ''; return $link; @@ -1220,10 +1302,16 @@ class UndeleteForm { if( !$file->userCan(File::DELETED_FILE) ) { return '' . $wgLang->timeanddate( $ts, true ) . ''; } 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 = '' . $link . ''; 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( '

      ' . htmlspecialchars( $err ) . "

      \n" ); } @@ -55,7 +55,7 @@ class DBUnlockForm { $action = $titleObj->escapeLocalURL( "action=submit" ); $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( <<addHTML( << @@ -74,7 +74,7 @@ class DBUnlockForm {
      -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,1028 +148,320 @@ 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 ) { - $this->processUpload(); - } else { - $this->mainUploadForm(); } - $this->cleanupTempFile(); - } - - /** - * Do the upload - * Checks are made in SpecialUpload::execute() - * - * @access private - */ - function processUpload(){ - global $wgUser, $wgOut, $wgFileExtensions, $wgLang; - $details = null; - $value = null; - $value = $this->internalProcessUpload( $details ); - - switch($value) { - case self::SUCCESS: - $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() ); - break; - - case self::BEFORE_PROCESSING: - break; - - case self::LARGE_FILE_SERVER: - $this->mainUploadForm( wfMsgHtml( 'largefileserver' ) ); - break; - - case self::EMPTY_FILE: - $this->mainUploadForm( wfMsgHtml( 'emptyfile' ) ); - break; - - case self::MIN_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; - - case self::OVERWRITE_EXISTING_FILE: - $errorText = $details['overwrite']; - $this->uploadError( $wgOut->parse( $errorText ) ); - break; - - case self::FILETYPE_MISSING: - $this->uploadError( wfMsgExt( 'filetype-missing', array ( 'parseinline' ) ) ); - break; - - case self::FILETYPE_BADTYPE: - $finalExt = $details['finalExt']; - $this->uploadError( - wfMsgExt( 'filetype-banned-type', - array( 'parseinline' ), - htmlspecialchars( $finalExt ), - $wgLang->commaList( $wgFileExtensions ), - $wgLang->formatNum( count($wgFileExtensions) ) - ) - ); - break; - - case self::VERIFICATION_ERROR: - $veri = $details['veri']; - $this->uploadError( $veri->toString() ); - break; - - case self::UPLOAD_VERIFICATION_ERROR: - $error = $details['error']; - $this->uploadError( $error ); - break; - - case self::UPLOAD_WARNING: - $warning = $details['warning']; - $this->uploadWarning( $warning ); - break; - - case self::INTERNAL_ERROR: - $internal = $details['internal']; - $this->showError( $internal ); - break; - - default: - throw new MWException( __METHOD__ . ": Unknown value `{$value}`" ); - } - } - - /** - * Really do the upload - * Checks are made in SpecialUpload::execute() - * - * @param array $resultDetails contains result-specific dict of additional values - * - * @access private - */ - function internalProcessUpload( &$resultDetails ) { - global $wgUser; - - if( !wfRunHooks( 'UploadForm:BeforeProcessing', array( &$this ) ) ) - { - wfDebug( "Hook 'UploadForm:BeforeProcessing' broke processing the file.\n" ); - return self::BEFORE_PROCESSING; - } - - /** - * If there was no filename or a zero size given, give up quick. - */ - if( trim( $this->mSrcName ) == '' || empty( $this->mFileSize ) ) { - return self::EMPTY_FILE; - } - - /* Check for curl error */ - if( $this->mCurlError ) { - return self::BEFORE_PROCESSING; - } - - /** - * Chop off any directories in the given filename. Then - * filter out illegal characters, and try to make a legible name - * out of it. We'll strip some silently that Title would die on. - */ - if( $this->mDesiredDestName ) { - $basename = $this->mDesiredDestName; - } else { - $basename = $this->mSrcName; - } - $filtered = wfStripIllegalFilenameChars( $basename ); - - /* Normalize to title form before we do any further processing */ - $nt = Title::makeTitleSafe( NS_FILE, $filtered ); - if( is_null( $nt ) ) { - $resultDetails = array( 'filtered' => $filtered ); - return self::ILLEGAL_FILENAME; - } - $filtered = $nt->getDBkey(); - - /** - * We'll want to blacklist against *any* 'extension', and use - * only the final one for the whitelist. - */ - list( $partname, $ext ) = $this->splitExtensions( $filtered ); - - if( count( $ext ) ) { - $finalExt = $ext[count( $ext ) - 1]; + # Process upload or show a form + if ( $this->mTokenOk && !$this->mCancelUpload + && ( $this->mUpload && $this->mUploadClicked ) ) { + $this->processUpload(); } else { - $finalExt = ''; - } - - # If there was more than one "extension", reassemble the base - # filename to prevent bogus complaints about length - if( count( $ext ) > 1 ) { - for( $i = 0; $i < count( $ext ) - 1; $i++ ) - $partname .= '.' . $ext[$i]; - } - - if( strlen( $partname ) < 1 ) { - return self::MIN_LENGTH_PARTNAME; - } - - $this->mLocalFile = wfLocalFile( $nt ); - $this->mDestName = $this->mLocalFile->getName(); - - /** - * If the image is protected, non-sysop users won't be able - * to modify it by uploading a new revision. - */ - $permErrors = $nt->getUserPermissionsErrors( 'edit', $wgUser ); - $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $wgUser ); - $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $wgUser ) ); - - if( $permErrors || $permErrorsUpload || $permErrorsCreate ) { - // merge all the problems into one list, avoiding duplicates - $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); - $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); - $resultDetails = array( 'permissionserrors' => $permErrors ); - return self::PROTECTED_PAGE; - } - - /** - * In some cases we may forbid overwriting of existing files. - */ - $overwrite = $this->checkOverwrite( $this->mDestName ); - if( $overwrite !== true ) { - $resultDetails = array( 'overwrite' => $overwrite ); - return self::OVERWRITE_EXISTING_FILE; - } - - /* Don't allow users to override the blacklist (check file extension) */ - global $wgCheckFileExtensions, $wgStrictFileExtensions; - global $wgFileExtensions, $wgFileBlacklist; - if ($finalExt == '') { - return self::FILETYPE_MISSING; - } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || - ($wgCheckFileExtensions && $wgStrictFileExtensions && - !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) ) { - $resultDetails = array( 'finalExt' => $finalExt ); - return self::FILETYPE_BADTYPE; - } - - /** - * Look at the contents of the file; if we can recognize the - * type but it's corrupt or data of the wrong type, we should - * probably not accept it. - */ - if( !$this->mStashed ) { - $this->mFileProps = File::getPropsFromPath( $this->mTempPath, $finalExt ); - $this->checkMacBinary(); - $veri = $this->verify( $this->mTempPath, $finalExt ); - - if( $veri !== true ) { //it's a wiki error... - $resultDetails = array( 'veri' => $veri ); - return self::VERIFICATION_ERROR; - } - - /** - * Provide an opportunity for extensions to add further checks - */ - $error = ''; - if( !wfRunHooks( 'UploadVerification', - array( $this->mDestName, $this->mTempPath, &$error ) ) ) { - $resultDetails = array( 'error' => $error ); - return self::UPLOAD_VERIFICATION_ERROR; - } - } - - - /** - * Check for non-fatal conditions - */ - if ( ! $this->mIgnoreWarning ) { - $warning = ''; - - $comparableName = str_replace( ' ', '_', $basename ); - global $wgCapitalLinks, $wgContLang; - if ( $wgCapitalLinks ) { - $comparableName = $wgContLang->ucfirst( $comparableName ); - } - - if( $comparableName !== $filtered ) { - $warning .= '
    • '.wfMsgHtml( 'badfilename', htmlspecialchars( $this->mDestName ) ).'
    • '; - } - - global $wgCheckFileExtensions; - if ( $wgCheckFileExtensions ) { - if ( !$this->checkFileExtension( $finalExt, $wgFileExtensions ) ) { - global $wgLang; - $warning .= '
    • ' . - wfMsgExt( 'filetype-unwanted-type', - array( 'parseinline' ), - htmlspecialchars( $finalExt ), - $wgLang->commaList( $wgFileExtensions ), - $wgLang->formatNum( count($wgFileExtensions) ) - ) . '
    • '; - } - } - - global $wgUploadSizeWarning; - if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) { - $skin = $wgUser->getSkin(); - $wsize = $skin->formatSize( $wgUploadSizeWarning ); - $asize = $skin->formatSize( $this->mFileSize ); - $warning .= '
    • ' . wfMsgHtml( 'large-file', $wsize, $asize ) . '
    • '; - } - if ( $this->mFileSize == 0 ) { - $warning .= '
    • '.wfMsgHtml( 'emptyfile' ).'
    • '; - } - - if ( !$this->mDestWarningAck ) { - $warning .= self::getExistsWarning( $this->mLocalFile ); + # Backwards compatibility hook + if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) + { + wfDebug( "Hook 'UploadForm:initial' broke output of the upload form" ); + return; } - $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; - } + $this->showUploadForm( $this->getUploadForm() ); } - /** - * Try actually saving the thing... - * It will show an error form on failure. - */ - 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; - } + # Cleanup + if ( $this->mUpload ) + $this->mUpload->cleanupTempFile(); } /** - * 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 + * Show the main upload form + * + * @param mixed $form An HTMLForm instance or HTML string to show */ - static function getExistsWarning( $file ) { - global $wgUser, $wgContLang; - // Check for uppercase extension. We allow these filenames but check if an image - // with lowercase extension exists already - $warning = ''; - $align = $wgContLang->isRtl() ? 'left' : 'right'; - - if( strpos( $file->getName(), '.' ) == false ) { - $partname = $file->getName(); - $rawExtension = ''; - } else { - $n = strrpos( $file->getName(), '.' ); - $rawExtension = substr( $file->getName(), $n + 1 ); - $partname = substr( $file->getName(), 0, $n ); + protected function showUploadForm( $form ) { + # Add links if file was previously deleted + if ( !$this->mDesiredDestName ) { + $this->showViewDeletedLinks(); } - - $sk = $wgUser->getSkin(); - - if ( $rawExtension != $file->getExtension() ) { - // We're not using the normalized form of the extension. - // Normal form is lowercase, using most common of alternate - // extensions (eg 'jpg' rather than 'JPEG'). - // - // Check for another file using the normalized form... - $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() ); - $file_lc = wfLocalFile( $nt_lc ); + + if ( $form instanceof HTMLForm ) { + $form->show(); } else { - $file_lc = false; - } - - if( $file->exists() ) { - $dlink = $sk->makeKnownLinkObj( $file->getTitle() ); - if ( $file->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $file->getTitle(), wfMsgExt( 'fileexists-thumb', 'parseinline' ), - $file->getName(), $align, array(), false, true ); - } elseif ( !$file->allowInlineDisplay() && $file->isSafeFile() ) { - $icon = $file->iconThumb(); - $dlink2 = '
      ' . - $icon->toHtml( array( 'desc-link' => true ) ) . '
      ' . $dlink . '
      '; - } else { - $dlink2 = ''; - } - - $warning .= '
    • ' . wfMsgExt( 'fileexists', array('parseinline','replaceafter'), $dlink ) . '
    • ' . $dlink2; - - } elseif( $file->getTitle()->getArticleID() ) { - $lnk = $sk->makeKnownLinkObj( $file->getTitle(), '', 'redirect=no' ); - $warning .= '
    • ' . wfMsgExt( 'filepageexists', array( 'parseinline', 'replaceafter' ), $lnk ) . '
    • '; - } 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 = '
      ' . - $icon->toHtml( array( 'desc-link' => true ) ) . '
      ' . $dlink . '
      '; - } else { - $dlink2 = ''; - } - - $warning .= '
    • ' . - wfMsgExt( 'fileexists-extension', 'parsemag', - $file->getTitle()->getPrefixedText(), $dlink ) . - '
    • ' . $dlink2; - - } elseif ( ( substr( $partname , 3, 3 ) == 'px-' || substr( $partname , 2, 3 ) == 'px-' ) - && ereg( "[0-9]{2}" , substr( $partname , 0, 2) ) ) - { - # Check for filenames like 50px- or 180px-, these are mostly thumbnails - $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension ); - $file_thb = wfLocalFile( $nt_thb ); - if ($file_thb->exists() ) { - # Check if an image without leading '180px-' (or similiar) exists - $dlink = $sk->makeKnownLinkObj( $nt_thb); - if ( $file_thb->allowInlineDisplay() ) { - $dlink2 = $sk->makeImageLinkObj( $nt_thb, - wfMsgExt( 'fileexists-thumb', 'parseinline' ), - $nt_thb->getText(), $align, array(), false, true ); - } elseif ( !$file_thb->allowInlineDisplay() && $file_thb->isSafeFile() ) { - $icon = $file_thb->iconThumb(); - $dlink2 = '
      ' . - $icon->toHtml( array( 'desc-link' => true ) ) . '
      ' . - $dlink . '
      '; - } else { - $dlink2 = ''; - } - - $warning .= '
    • ' . wfMsgExt( 'fileexists-thumbnail-yes', 'parsemag', $dlink ) . - '
    • ' . $dlink2; - } else { - # Image w/o '180px-' does not exists, but we do not like these filenames - $warning .= '
    • ' . wfMsgExt( 'file-thumbnail-no', 'parseinline' , - substr( $partname , 0, strpos( $partname , '-' ) +1 ) ) . '
    • '; - } - } - - $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist(); - # Do the match - foreach( $filenamePrefixBlacklist as $prefix ) { - if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { - $warning .= '
    • ' . wfMsgExt( 'filename-bad-prefix', 'parseinline', $prefix ) . '
    • '; - break; - } - } - - if ( $file->wasDeleted() && !$file->exists() ) { - # If the file existed before and was deleted, warn the user of this - # Don't bother doing so if the file exists now, however - $ltitle = SpecialPage::getTitleFor( 'Log' ); - $llink = $sk->makeKnownLinkObj( $ltitle, wfMsgHtml( 'deletionlog' ), - 'type=delete&page=' . $file->getTitle()->getPrefixedUrl() ); - $warning .= '
    • ' . wfMsgWikiHtml( 'filewasdeleted', $llink ) . '
    • '; + global $wgOut; + $wgOut->addHTML( $form ); } - return $warning; + } /** - * Get a list of warnings + * Get an UploadForm instance with title and text properly set. * - * @param string local filename, e.g. 'file exists', 'non-descriptive filename' - * @return array list of warning messages + * @param string $message HTML string to add to the form + * @param string $sessionKey Session key in case this is a stashed upload + * @return UploadForm */ - static function ajaxGetExistsWarning( $filename ) { - $file = wfFindFile( $filename ); - if( !$file ) { - // Force local file so we have an object to do further checks against - // if there isn't an exact match... - $file = wfLocalFile( $filename ); - } - $s = ' '; - if ( $file ) { - $warning = self::getExistsWarning( $file ); - if ( $warning !== '' ) { - $s = "
        $warning
      "; - } + 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( '
      ' . + wfMsgExt( 'uploadtext', 'parse', array( $this->mDesiredDestName ) ) . + '
      ' ); + # Add upload error message + $form->addPreText( $message ); + + # Add footer to form + $uploadFooter = wfMsgNoTrans( 'uploadfooter' ); + if ( $uploadFooter != '-' && !wfEmptyMsg( 'uploadfooter', $uploadFooter ) ) { + $form->addPostText( '\n" ); } - 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 $form; - return $output->getText(); } - + /** - * Check for duplicate files and throw up a warning before the upload - * completes. + * Shows the "view X deleted revivions link"" */ - function getDupeWarning( $tempfile, $extension, $destinationTitle ) { - $hash = File::sha1Base36( $tempfile ); - $dupes = RepoGroup::singleton()->findBySha1( $hash ); - $archivedImage = new ArchivedFile( null, 0, $hash.".$extension" ); - if( $dupes ) { - global $wgOut; - $msg = ""; - 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"; - } + 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 ) + ) + ); + $wgOut->addHTML( "
      {$link}
      " ); } - $msg .= "
      "; - return "
    • " . - wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) . - $wgOut->parse( $msg ) . - "
    • \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 ); - } + // 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() ); } - return $blacklist; } /** - * Stash a file in a temporary directory for later processing - * after the user has confirmed it. + * Stashes the upload and shows the main upload form. * - * If the user doesn't explicitly cancel or accept, these files - * can accumulate in the temp directory. + * 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. * - * @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 + * @param string $message HTML message to be passed to mainUploadForm */ - 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; - } + protected function showRecoverableUploadError( $message ) { + $sessionKey = $this->mUpload->stashSession(); + $message = '

      ' . wfMsgHtml( 'uploadwarning' ) . "

      \n" . + '
      ' . $message . "
      \n"; + + $form = $this->getUploadForm( $message, $sessionKey ); + $form->setSubmitText( wfMsg( 'upload-tryagain' ) ); + $this->showUploadForm( $form ); } - /** - * 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. + * Stashes the upload, shows the main form, but adds an "continue anyway button". + * Also checks whether there are actually warnings to display. * - * @return int - * @access private + * @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 stashSession() { - $stash = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); + protected function showUploadWarning( $warnings ) { + global $wgUser; - if( !$stash ) { - # Couldn't save the file. + # 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 ) ) ) + { return false; } - $key = mt_rand( 0, 0x7fffffff ); - $_SESSION['wsUploadData'][$key] = array( - 'mTempPath' => $stash, - 'mFileSize' => $this->mFileSize, - 'mSrcName' => $this->mSrcName, - 'mFileProps' => $this->mFileProps, - 'version' => self::SESSION_VERSION, - ); - return $key; - } + $sessionKey = $this->mUpload->stashSession(); - /** - * 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; + $sk = $wgUser->getSkin(); + + $warningHtml = '

      ' . wfMsgHtml( 'uploadwarning' ) . "

      \n" + . '
        '; + foreach( $warnings as $warning => $args ) { + $msg = ''; + if( $warning == 'exists' ) { + $msg = "\t
      • " . self::getExistsWarning( $args ) . "
      • \n"; + } elseif( $warning == 'duplicate' ) { + $msg = self::getDupeWarning( $args ); + } elseif( $warning == 'duplicate-archive' ) { + $msg = "\t
      • " . wfMsgExt( 'file-deleted-duplicate', 'parseinline', + array( Title::makeTitle( NS_FILE, $args )->getPrefixedText() ) ) + . "
      • \n"; + } else { + if ( $args === true ) + $args = array(); + elseif ( !is_array( $args ) ) + $args = array( $args ); + $msg = "\t
      • " . wfMsgExt( $warning, 'parseinline', $args ) . "
      • \n"; + } + $warningHtml .= $msg; } - } + $warningHtml .= "
      \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' ) ); - /** - * @param string $error as HTML - * @access private - */ - function uploadError( $error ) { - global $wgOut; - $wgOut->addHTML( '

      ' . wfMsgHtml( 'uploadwarning' ) . "

      \n" ); - $wgOut->addHTML( '' . $error . '' ); + $this->showUploadForm( $form ); + + # Indicate that we showed a form + return true; } /** - * 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. + * Show the upload form with error message, but do not stash the file. * - * @param string $warning as HTML - * @access private + * @param string $message */ - function uploadWarning( $warning ) { - global $wgOut; - global $wgUseCopyrightUpload; - - $this->mSessionKey = $this->stashSession(); - if( !$this->mSessionKey ) { - # Couldn't save file; an error has been displayed so let's go. - return; - } - - $wgOut->addHTML( '

      ' . wfMsgHtml( 'uploadwarning' ) . "

      \n" ); - $wgOut->addHTML( '
        ' . $warning . "
      \n" ); - - $titleObj = SpecialPage::getTitleFor( 'Upload' ); - - if ( $wgUseCopyrightUpload ) { - $copyright = Xml::hidden( 'wpUploadCopyStatus', $this->mCopyrightStatus ) . "\n" . - Xml::hidden( 'wpUploadSource', $this->mCopyrightSource ) . "\n"; - } else { - $copyright = ''; - } - - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL( 'action=submit' ), - 'enctype' => 'multipart/form-data', 'id' => 'uploadwarning' ) ) . "\n" . - Xml::hidden( 'wpIgnoreWarning', '1' ) . "\n" . - Xml::hidden( 'wpSessionKey', $this->mSessionKey ) . "\n" . - Xml::hidden( 'wpUploadDescription', $this->mComment ) . "\n" . - Xml::hidden( 'wpLicense', $this->mLicense ) . "\n" . - Xml::hidden( 'wpDestFile', $this->mDesiredDestName ) . "\n" . - Xml::hidden( 'wpWatchthis', $this->mWatchthis ) . "\n" . - "{$copyright}
      " . - Xml::submitButton( wfMsg( 'ignorewarning' ), array ( 'name' => 'wpUpload', 'id' => 'wpUpload', 'checked' => 'checked' ) ) . ' ' . - Xml::submitButton( wfMsg( 'reuploaddesc' ), array ( 'name' => 'wpReUpload', 'id' => 'wpReUpload' ) ) . - Xml::closeElement( 'form' ) . "\n" - ); + protected function showUploadError( $message ) { + $message = '

      ' . wfMsgHtml( 'uploadwarning' ) . "

      \n" . + '
      ' . $message . "
      \n"; + $this->showUploadForm( $this->getUploadForm( $message ) ); } /** - * Displays the main upload form, optionally with a highlighted - * error message up at the top. - * - * @param string $msg as HTML - * @access private + * Do the upload. + * Checks are made in SpecialUpload::execute() */ - 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( "" ); - $wgOut->addScriptFile( 'upload.js' ); - $wgOut->addScriptFile( 'edit.js' ); // For support + protected function processUpload() { + global $wgUser, $wgOut; - if( !wfRunHooks( 'UploadForm:initial', array( &$this ) ) ) - { - wfDebug( "Hook 'UploadForm:initial' broke output of the upload form\n" ); - return false; + // Verify permissions + $permErrors = $this->mUpload->verifyPermissions( $wgUser ); + if( $permErrors !== true ) { + $wgOut->showPermissionsErrorPage( $permErrors ); + return; } - 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( "
      {$link}
      " ); - } - - // 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() ); - } + // Fetch the file if required + $status = $this->mUpload->fetchFile(); + if( !$status->isOK() ) { + $this->showUploadForm( $this->getUploadForm( $wgOut->parse( $status->getWikiText() ) ) ); + return; } - $cols = intval($wgUser->getOption( 'cols' )); - - if( $wgUser->getOption( 'editwidth' ) ) { - $width = " style=\"width:100%\""; - } else { - $width = ''; + // 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 ); } - if ( '' != $msg ) { - $sub = wfMsgHtml( 'uploaderror' ); - $wgOut->addHTML( "

      {$sub}

      \n" . - "{$msg}\n" ); + + // Upload verification + $details = $this->mUpload->verifyUpload(); + if ( $details['status'] != UploadBase::OK ) { + $this->processVerificationError( $details ); + return; } - $wgOut->addHTML( '
      ' ); - $wgOut->addWikiMsg( 'uploadtext', $this->mDesiredDestName ); - $wgOut->addHTML( "
      \n" ); - # Print a list of allowed file extensions, if so configured. We ignore - # MIME type here, it's incomprehensible to most people and too long. - global $wgCheckFileExtensions, $wgStrictFileExtensions, - $wgFileExtensions, $wgFileBlacklist; + $this->mLocalFile = $this->mUpload->getLocalFile(); - $allowedExtensions = ''; - if( $wgCheckFileExtensions ) { - if( $wgStrictFileExtensions ) { - # Everything not permitted is banned - $extensionsList = - '
      ' . - wfMsgWikiHtml( 'upload-permitted', $wgLang->commaList( $wgFileExtensions ) ) . - "
      \n"; - } else { - # We have to list both preferred and prohibited - $extensionsList = - '
      ' . - wfMsgWikiHtml( 'upload-preferred', $wgLang->commaList( $wgFileExtensions ) ) . - "
      \n" . - '
      ' . - wfMsgWikiHtml( 'upload-prohibited', $wgLang->commaList( $wgFileBlacklist ) ) . - "
      \n"; + // Check warnings if necessary + if( !$this->mIgnoreWarning ) { + $warnings = $this->mUpload->checkWarnings(); + if( $this->showUploadWarning( $warnings ) ) { + return; } - } else { - # Everything is permitted. - $extensionsList = ''; } - # Get the maximum file size from php.ini as $wgMaxUploadSize works for uploads from URL via CURL only - # See http://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize for possible values of upload_max_filesize - $val = trim( ini_get( 'upload_max_filesize' ) ); - $last = strtoupper( ( substr( $val, -1 ) ) ); - switch( $last ) { - case 'G': - $val2 = substr( $val, 0, -1 ) * 1024 * 1024 * 1024; - break; - case 'M': - $val2 = substr( $val, 0, -1 ) * 1024 * 1024; - break; - case 'K': - $val2 = substr( $val, 0, -1 ) * 1024; - break; - default: - $val2 = $val; - } - $val2 = $wgAllowCopyUploads ? min( $wgMaxUploadSize, $val2 ) : $val2; - $maxUploadSize = '
      ' . - wfMsgExt( 'upload-maxfilesize', array( 'parseinline', 'escapenoentities' ), - $wgLang->formatSize( $val2 ) ) . - "
      \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 = - "" . - "" . - wfMsgHTML( 'upload_source_file' ) . "
      " . - "" . - "" . - wfMsgHtml( 'upload_source_url' ) ; - } else { - $filename_form = - "mDesiredDestName?"":"onchange='fillDestFilename(\"wpUploadFile\")' ") . - "size='60' />" . - "" ; - } - if ( $useAjaxDestCheck ) { - $warningRow = " "; - $destOnkeyup = 'onkeyup="wgUploadWarningObj.keypress();"'; + // Get the page text if this is not a reupload + if( !$this->mForReUpload ) { + $pageText = self::getInitialPageText( $this->mComment, $this->mLicense, + $this->mCopyrightStatus, $this->mCopyrightSource ); } else { - $warningRow = ''; - $destOnkeyup = ''; + $pageText = false; } - $encComment = htmlspecialchars( $this->mComment ); - - - $wgOut->addHTML( - Xml::openElement( 'form', array( 'method' => 'post', 'action' => $titleObj->getLocalURL(), - 'enctype' => 'multipart/form-data', 'id' => 'mw-upload-form' ) ) . - Xml::openElement( 'fieldset' ) . - Xml::element( 'legend', null, wfMsg( 'upload' ) ) . - Xml::openElement( 'table', array( 'border' => '0', 'id' => 'mw-upload-table' ) ) . - " - {$this->uploadFormTextTop} - - - - - {$filename_form} - - - - - - {$maxUploadSize} - {$extensionsList} - - - - - - - " - ); - if( $this->mForReUpload ) { - $wgOut->addHTML( - Xml::hidden( 'wpDestFile', $this->mDesiredDestName, array('id'=>'wpDestFile','tabindex'=>2) ) . - "" . - $encDestName . - "" - ); - } - else { - $wgOut->addHTML( - "" - ); + $status = $this->mUpload->performUpload( $this->mComment, $pageText, $this->mWatchthis, $wgUser ); + if ( !$status->isGood() ) { + $this->showUploadError( $wgOut->parse( $status->getWikiText() ) ); + return; } - - $wgOut->addHTML( - " - - - - - - - - {$this->uploadFormTextAfterSummary} - - - " - ); - # Re-uploads should not need license info - if ( !$this->mForReUpload && $licenseshtml != '' ) { - global $wgStylePath; - $wgOut->addHTML( " - - - - - - - - " - ); - if( $useAjaxLicensePreview ) { - $wgOut->addHTML( " - - - - " - ); - } - } + // Success, redirect to description page + $this->mUploadSuccessful = true; + wfRunHooks( 'SpecialUploadComplete', array( &$this ) ); + $wgOut->redirect( $this->mLocalFile->getTitle()->getFullURL() ); - if ( !$this->mForReUpload && $wgUseCopyrightUpload ) { - $filestatus = wfMsgExt( 'filestatus', 'escapenoentities' ); - $copystatus = htmlspecialchars( $this->mCopyrightStatus ); - $filesource = wfMsgExt( 'filesource', 'escapenoentities' ); - $uploadsource = htmlspecialchars( $this->mCopyrightSource ); - - $wgOut->addHTML( " - - - - - - - - - - - - - - - " - ); - } + } - $wgOut->addHTML( " - - - - - - - - - $warningRow - - - - getSkin()->tooltipAndAccesskey( 'upload' ) . " /> - - - - - " - ); - $wgOut->addHTML( '
      ' ); - $wgOut->addWikiMsgArray( 'edittools', array(), array( 'content' ) ); - $wgOut->addHTML( '
      ' ); - $wgOut->addHTML( " - - " . - 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( '' ); + /** + * Get the initial image page text based on a comment and optional file status information + */ + 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 { + if ( $license != '' ) { + $filedesc = $comment == '' ? '' : '== ' . wfMsgForContent ( 'filedesc' ) . " ==\n" . $comment . "\n"; + $pageText = $filedesc . + '== ' . wfMsgForContent ( 'license-header' ) . " ==\n" . '{{' . $license . '}}' . "\n"; + } else { + $pageText = $comment; + } } + return $pageText; } - /* -------------------------------------------------------------- */ - /** * 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 @@ -1292,13 +473,13 @@ wgUploadAutoFill = {$autofill}; * Note that the page target can be changed *on the form*, so our check * state can get out of sync. */ - function watchCheck() { + protected function getWatchCheck() { 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. @@ -1310,540 +491,569 @@ wgUploadAutoFill = {$autofill}; } } - /** - * 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. + * Provides output to the user for a result of UploadBase::verifyUpload * - * @param array $ext - * @param array $list - * @return bool + * @param array $details Result of UploadBase::verifyUpload */ - public function checkFileExtensionList( $ext, $list ) { - foreach( $ext as $e ) { - if( in_array( strtolower( $e ), $list ) ) { - return true; - } - } - return false; - } + protected function processVerificationError( $details ) { + global $wgFileExtensions, $wgLang; - /** - * 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' ); - } + switch( $details['status'] ) { - #check mime type blacklist - global $wgMimeTypeBlacklist; - if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist) ) { - if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { - return new WikiErrorMsg( 'filetype-badmime', htmlspecialchars( $mime ) ); - } + /** 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; - # 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 ); - } + /** 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 { + $error = $details['error']; + $args = null; } - } - } - - #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) ); + $this->showUploadError( wfMsgExt( $error, 'parseinline', $args ) ); + break; + default: + throw new MWException( __METHOD__ . ": Unknown value `{$details['status']}`" ); } - - wfDebug( __METHOD__.": all clear; passing.\n" ); - return true; } /** - * Checks if the mime type of the uploaded file matches the file extension. - * - * @param string $mime the mime type of the uploaded file - * @param string $extension The filename extension that the file is to be served with - * @return bool + * Remove a temporarily kept file stashed by saveTempUploadedFile(). + * @access private + * @return success */ - 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! + protected function unsaveUploadedFile() { + global $wgOut; + if ( !( $this->mUpload instanceof UploadFromStash ) ) return true; - - } else { - wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); + $success = $this->mUpload->unsaveUploadedFile(); + if ( ! $success ) { + $wgOut->showFileDeleteError( $this->mUpload->getTempPath() ); return false; + } else { + return true; } } + /*** Functions for formatting warnings ***/ /** - * 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. + * Formats a result of UploadBase::getExistsWarning as HTML + * This check is static and can be done pre-upload via AJAX * - * @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 + * @param array $exists The result of UploadBase::getExistsWarning + * @return string Empty string if there is no warning or an HTML fragment */ - function detectScript($file, $mime, $extension) { - global $wgAllowTitlesInSVG; + public static function getExistsWarning( $exists ) { + global $wgUser, $wgContLang; - #ugly hack: for text files, always look at the entire file. - #For binarie field, just check the first K. + if ( !$exists ) + return ''; - if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file ); - else { - $fp = fopen( $file, 'rb' ); - $chunk = fread( $fp, 1024 ); - fclose( $fp ); - } + $file = $exists['file']; + $filename = $file->getTitle()->getPrefixedText(); + $warning = ''; - $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("getSkin(); - foreach( $tags as $tag ) { - if( false !== strpos( $chunk, $tag ) ) { - return true; - } + 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 + $ltitle = SpecialPage::getTitleFor( 'Log' ); + $llink = $sk->linkKnown( + $ltitle, + wfMsgHtml( 'deletionlog' ), + array(), + array( + 'type' => 'delete', + 'page' => $filename + ) + ); + $warning = wfMsgWikiHtml( 'filewasdeleted', $llink ); } - /* - * 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; + return $warning; } - function detectScriptInSvg( $filename ) { - $check = new XmlTypeCheck( $filename, array( $this, 'checkSvgScriptCallback' ) ); - return $check->filterMatch; - } - /** - * @todo Replace this with a whitelist filter! + * Get a list of warnings + * + * @param string local filename, e.g. 'file exists', 'non-descriptive filename' + * @return array list of warning messages */ - function checkSvgScriptCallback( $element, $attribs ) { - $stripped = $this->stripXmlNamespace( $element ); - - if( $stripped == 'script' ) { - wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" ); - return true; + public static function ajaxGetExistsWarning( $filename ) { + $file = wfFindFile( $filename ); + if( !$file ) { + // Force local file so we have an object to do further checks against + // if there isn't an exact match... + $file = wfLocalFile( $filename ); } - - 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; + $s = ' '; + if ( $file ) { + $exists = UploadBase::getExistsWarning( $file ); + $warning = self::getExistsWarning( $exists ); + if ( $warning !== '' ) { + $s = "
      $warning
      "; } } + return $s; } - - 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. + * Construct a warning and a gallery from an array of duplicate files. */ - 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( '
      $1
      ', 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 ); + public static function getDupeWarning( $dupes ) { + if( $dupes ) { + global $wgOut; + $msg = ""; + foreach( $dupes as $file ) { + $title = $file->getTitle(); + $msg .= $title->getPrefixedText() . + "|" . $title->getText() . "\n"; + } + $msg .= ""; + return "
    • " . + wfMsgExt( "file-exists-duplicate", array( "parse" ), count( $dupes ) ) . + $wgOut->parse( $msg ) . + "
    • \n"; } else { - # complex pattern: replace "%f" with file to scan - $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); + return ''; } + } - wfDebug( __METHOD__.": running virus scan: $command \n" ); +} - # execute virus scanner - $exitCode = false; +/** + * 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; - #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 ); - } + public function __construct( $options = array() ) { + global $wgLang; - # map exit code to AV_xxx constants. - $mappedCode = $exitCode; - if ( $exitCodeMap ) { - if ( isset( $exitCodeMap[$exitCode] ) ) { - $mappedCode = $exitCodeMap[$exitCode]; - } elseif ( isset( $exitCodeMap["*"] ) ) { - $mappedCode = $exitCodeMap["*"]; - } - } + $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'] : ''; - if ( $mappedCode === AV_SCAN_FAILED ) { - # scan failed (code was mapped to false by $exitCodeMap) - wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" ); + $sourceDescriptor = $this->getSourceSection(); + $descriptor = $sourceDescriptor + + $this->getDescriptionSection() + + $this->getOptionsSection(); - 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]; - } - } - } + 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' ); - wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output \n" ); - return $output; + # Build a list of IDs for javascript insertion + $this->mSourceIds = array(); + foreach ( $sourceDescriptor as $key => $field ) { + if ( !empty( $field['id'] ) ) + $this->mSourceIds[] = $field['id']; } + } /** - * 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 + * Get the descriptor of the fieldset that contains the file source + * selection. The section is 'source' + * + * @return array Descriptor array */ - 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 ); + 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', + ), + ); + } - $this->mTempPath = $dataFile; - $this->mFileSize = $macbin->dataForkLength(); + $canUploadByUrl = UploadFromUrl::isEnabled() && $wgUser->isAllowed( 'upload_by_url' ); + $radio = $canUploadByUrl; + $selectedSourceType = strtolower( $wgRequest->getText( 'wpSourceType', 'File' ) ); - // We'll have to manually remove the new file if it's not kept. - $this->mRemoveTempFile = true; + $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', + ); } - $macbin->close(); + wfRunHooks( 'UploadFormSourceDescriptors', array( &$descriptor, &$radio, $selectedSourceType ) ); + + $descriptor['Extensions'] = array( + 'type' => 'info', + 'section' => 'source', + 'default' => $this->getExtensionsMessage(), + 'raw' => true, + ); + return $descriptor; } + /** - * If we've modified the upload file we need to manually remove it - * on exit to clean up. - * @access private + * Get the messages indicating which extensions are preferred and prohibitted. + * + * @return string HTML string containing the message */ - function cleanupTempFile() { - if ( $this->mRemoveTempFile && $this->mTempPath && file_exists( $this->mTempPath ) ) { - wfDebug( "SpecialUpload::cleanupTempFile: Removing temporary file {$this->mTempPath}\n" ); - unlink( $this->mTempPath ); + 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 $wgLang, $wgCheckFileExtensions, $wgStrictFileExtensions, + $wgFileExtensions, $wgFileBlacklist; + + $allowedExtensions = ''; + if( $wgCheckFileExtensions ) { + if( $wgStrictFileExtensions ) { + # Everything not permitted is banned + $extensionsList = + '
      ' . + wfMsgWikiHtml( 'upload-permitted', $wgLang->commaList( $wgFileExtensions ) ) . + "
      \n"; + } else { + # We have to list both preferred and prohibited + $extensionsList = + '
      ' . + wfMsgWikiHtml( 'upload-preferred', $wgLang->commaList( $wgFileExtensions ) ) . + "
      \n" . + '
      ' . + wfMsgWikiHtml( 'upload-prohibited', $wgLang->commaList( $wgFileBlacklist ) ) . + "
      \n"; + } + } else { + # Everything is permitted. + $extensionsList = ''; } + return $extensionsList; } /** - * 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 + * Get the descriptor of the fieldset that contains the file description + * input. The section is 'description' + * + * @return array Descriptor array */ - 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"; - } - } + protected function getDescriptionSection() { + global $wgUser, $wgOut; + + $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->mTextAfterSummary ) { + $descriptor['UploadFormTextAfterSummary'] = array( + 'type' => 'info', + 'section' => 'description', + 'default' => $this->mTextAfterSummary, + 'raw' => true, + ); } + + $descriptor += array( + 'EditTools' => array( + 'type' => 'edittools', + 'section' => 'description', + ), + 'License' => array( + 'type' => 'select', + 'class' => 'Licenses', + 'section' => 'description', + 'id' => 'wpLicense', + 'label-message' => 'license', + ), + ); + if ( $this->mForReUpload ) + $descriptor['DestFile']['readonly'] = true; - if( $error ) { - $errorText = wfMsg( $error, wfEscapeWikiText( $img->getName() ) ); - return $errorText; + 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', + ); } - // Rockin', go ahead and upload - return true; + return $descriptor; } - /** - * Check if a user is the last uploader - * - * @param User $user - * @param string $img, image name - * @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 */ - public static function userCanReUpload( User $user, $img ) { - if( $user->isAllowed( 'reupload' ) ) - return true; // non-conditional - if( !$user->isAllowed( 'reupload-own' ) ) - return false; + protected function getOptionsSection() { + global $wgUser, $wgOut; + + if( $wgUser->isLoggedIn() ) { + $descriptor = array( + 'Watchthis' => array( + 'type' => 'check', + 'id' => 'wpWatchthis', + 'label-message' => 'watchthisupload', + 'section' => 'options', + 'default' => $wgUser->getOption( 'watchcreations' ), + ) + ); + } + if( !$this->mHideIgnoreWarning ) { + $descriptor['IgnoreWarning'] = array( + 'type' => 'check', + 'id' => 'wpIgnoreWarning', + 'label-message' => 'ignorewarnings', + 'section' => 'options', + ); + } - $dbr = wfGetDB( DB_SLAVE ); - $row = $dbr->selectRow('image', - /* SELECT */ 'img_user', - /* WHERE */ array( 'img_name' => $img ) + $descriptor['wpDestFileWarningAck'] = array( + 'type' => 'hidden', + 'id' => 'wpDestFileWarningAck', + 'default' => $this->mDestWarningAck ? '1' : '', ); - if ( !$row ) - return false; + + if ( $this->mForReUpload ) { + $descriptor['wpForReUpload'] = array( + 'type' => 'hidden', + 'id' => 'wpForReUpload', + 'default' => '1', + ); + } + + return $descriptor; - return $user->getId() == $row->img_user; } /** - * Display an error with a wikitext description + * Add the upload JS and show the form. */ - function showError( $description ) { - global $wgOut; - $wgOut->setPageTitle( wfMsg( "internalerror" ) ); - $wgOut->setRobotPolicy( "noindex,nofollow" ); - $wgOut->setArticleRelated( false ); - $wgOut->enableClientCache( false ); - $wgOut->addWikiText( $description ); + public function show() { + $this->addUploadJS(); + parent::show(); } /** - * Get the initial image page text based on a comment and optional file status information + * Add upload JS to $wgOut + * + * @param bool $autofill Whether or not to autofill the destination + * filename text box */ - 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; + protected function addUploadJS( ) { + global $wgUseAjax, $wgAjaxUploadDestCheck, $wgAjaxLicensePreview, $wgEnableAPI; + global $wgOut; + + $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, + ); + + $wgOut->addScript( Skin::makeVariablesScript( $scriptVars ) ); + + // For support + $wgOut->addScriptFile( 'edit.js' ); + $wgOut->addScriptFile( 'upload.js' ); } /** - * 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 + * Empty function; submission is handled elsewhere. + * + * @return bool false */ - 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( '
      ' ); - $out->addWikiMsg( 'upload-wasdeleted' ); - $out->addHTML( - $loglist->beginLogEventsList() . - $pager->getBody() . - $loglist->endLogEventsList() + function trySubmit() { + return false; + } + +} + +/** + * 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( '
      ' ); + 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 @@ -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'], '&' ) !== 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 = '

      ' . $grouplist . ' ' . $wgLang->listToText( $list ) . '

      '; + $grouplist = '

      ' . $grouplist . ' ' . $wgLang->listToText( $list ) . "

      \n"; + } + if( count( $autolist ) > 0 ) { + $autogrouplistintro = wfMsgHtml( 'userrights-groupsmember-auto' ); + $grouplist .= '

      ' . $autogrouplistintro . ' ' . $wgLang->listToText( $autolist ) . "

      \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
      \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
      \n"; + $columns['changeable'][$group] = $checkbox; } } - if ($column) { - $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) . - " -"; - 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.= " - -"; - if( $settable_col !== '' ) { - $ret .= -" - $settable_col - -"; - } - if( $unsettable_col !== '' ) { - $ret .= -" - $unsettable_col - -"; + # Build the HTML table + $ret .= Xml::openElement( 'table', array( 'border' => '0', 'class' => 'mw-userrights-groups' ) ) . + "\n"; + foreach( $columns as $name => $column ) { + if( $column === array() ) + continue; + $ret .= xml::element( 'th', null, wfMsg( 'userrights-' . $name . '-col' ) ); + } + $ret.= "\n\n"; + foreach( $columns as $column ) { + if( $column === array() ) + continue; + $ret .= "\t\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 + ) . "
      \n"; } - $ret .= Xml::closeElement( 'tr' ) . Xml::closeElement( 'table' ); + $ret .= "\t\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( '
      ' ); + $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 .= '
      '; + $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 .= '
      '; 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' ) ) . " " . wfMsg( 'version-software-product' ) . " " . wfMsg( 'version-software-version' ) . " - \n - - [http://www.mediawiki.org/ MediaWiki] - " . self::getVersionLinked() . " - \n - - [http://www.php.net/ PHP] - " . phpversion() . " (" . php_sapi_name() . ") - \n - - " . $dbr->getSoftwareLink() . " - " . $dbr->getServerVersion() . " - \n" . - Xml::closeElement( 'table' ); + \n"; + foreach( $software as $name => $version ) { + $out .= " + " . $name . " + " . $version . " + \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 .= '' . $this->listToText( $wgExtensionFunctions ) . "\n"; + $out .= $this->openExtType( wfMsg( 'version-extension-functions' ), 'extension-functions' ); + $out .= '' . $this->listToText( $wgExtensionFunctions ) . "\n"; } if ( $cnt = count( $tags = $wgParser->getTags() ) ) { for ( $i = 0; $i < $cnt; ++$i ) $tags[$i] = "<{$tags[$i]}>"; - $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ) ); - $out .= '' . $this->listToText( $tags ). "\n"; + $out .= $this->openExtType( wfMsg( 'version-parser-extensiontags' ), 'parser-tags' ); + $out .= '' . $this->listToText( $tags ). "\n"; } if( $cnt = count( $fhooks = $wgParser->getFunctionHooks() ) ) { - $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ) ); - $out .= '' . $this->listToText( $fhooks ) . "\n"; + $out .= $this->openExtType( wfMsg( 'version-parser-function-hooks' ), 'parser-function-hooks' ); + $out .= '' . $this->listToText( $fhooks ) . "\n"; } if ( count( $wgSkinExtensionFunctions ) ) { - $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ) ); - $out .= '' . $this->listToText( $wgSkinExtensionFunctions ) . "\n"; + $out .= $this->openExtType( wfMsg( 'version-skin-extension-functions' ), 'skin-extension-functions' ); + $out .= '' . $this->listToText( $wgSkinExtensionFunctions ) . "\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 = '' . + wfMsg( 'version-version', $extension['version'] ) . + ''; + } 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 " - $extension $version - $description - " . $this->listToText( (array)$author ) . " + if ( $svnText !== false ) { + $extNameVer = " + $mainLink $versionText + $svnText"; + } else { + $extNameVer = " + $mainLink $versionText"; + } + $author = isset ( $extension['author'] ) ? $extension['author'] : array(); + $extDescAuthor = "$description + " . $this->listToText( (array)$author, false ) . " \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' ) ) . " " . wfMsg( 'version-hook-name' ) . " " . wfMsg( 'version-hook-subscribedby' ) . " @@ -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 .= '' . Xml::element( 'td', $opt ) . "\n"; } $this->firstExtOpened = false; - if($name) { $opt['id'] = "sv-$name"; } + if( $name ) + $opt['id'] = "sv-$name"; - $out .= "" . Xml::element( 'th', $opt, $text) . "\n"; + $out .= "" . Xml::element( 'th', $opt, $text ) . "\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() - ? '' . $skin->makeLinkObj( $title ) . '' - : $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 ) . "
      \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 @@ -6,41 +6,30 @@ * @ingroup SpecialPage */ -/** - * Entry point - * @param $par String: An article name ?? - */ -function wfSpecialWhatlinkshere($par = NULL) { - global $wgRequest; - $page = new WhatLinksHerePage( $wgRequest, $par ); - $page->execute(); -} - /** * implements Special:Whatlinkshere * @ingroup SpecialPage */ -class WhatLinksHerePage { - // Stored data - protected $par; +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 ){ } ?> - + MediaWiki <?php echo htmlspecialchars( $wgVersion ) ?> - -data['usercss']) { ?> - -data['userjs']) { ?> - -data['userjsprev']) { ?> - -data['trackbackhtml']) print $this->data['trackbackhtml']; ?> - -data['body_ondblclick']) { ?> ondblclick="text('body_ondblclick') ?>" -data['body_onload']) { ?> onload="text('body_onload') ?>" - class="mediawiki text('dir') ?> text('pageclass') ?> text('skinnameclass') ?>"> + $this->html( 'headelement' ); - -
      - -
      -
        - $url) { - if ((isset($wgArchNavBarSelected) && $this->data['title'] == $name && in_array($name, $wgArchNavBarSelected)) - || (!(isset($wgArchNavBarSelected) && in_array($this->data['title'], $wgArchNavBarSelected)) && isset($wgArchNavBarSelectedDefault) && $name == $wgArchNavBarSelectedDefault)) { - $anbClass = ' class="anb-selected"'; - } else { - $anbClass = ''; - } - echo '
      • '.$name.'
      • '; +if (empty($_REQUEST['printable'])) {?> +
        + +
        +
          + $url) { + if ((isset($wgArchNavBarSelected) && $this->data['title'] == $name && in_array($name, $wgArchNavBarSelected)) + || (!(isset($wgArchNavBarSelected) && in_array($this->data['title'], $wgArchNavBarSelected)) && isset($wgArchNavBarSelectedDefault) && $name == $wgArchNavBarSelectedDefault)) { + $anbClass = ' class="anb-selected"'; + } else { + $anbClass = ''; } + echo '
        • '.$name.'
        • '; } - ?> -
        -
        -
        - - -
        -
        -
        - - data['sitenotice']) { ?>
        html('sitenotice') ?>
        -

        data['displaytitle']!=""?$this->html('title'):$this->text('title') ?>

        -
        -

        msg('tagline') ?>

        -
        html('subtitle') ?>
        - data['undelete']) { ?>
        html('undelete') ?>
        - data['newtalk'] ) { ?>
        html('newtalk') ?>
        - data['showjumplinks']) { ?> - - html('bodytext') ?> - data['catlinks']) { $this->html('catlinks'); } ?> - - data['dataAfterContent']) { $this->html ('dataAfterContent'); } ?> -
        -
        + } + ?> +
      -
      -
      +
      +
      +
      html("specialpageattributes") ?>> + + data['sitenotice']) { ?>
      html('sitenotice') ?>
      + +

      html('title') ?>

      +
      +

      msg('tagline') ?>

      +
      html('userlangattributes') ?>>html('subtitle') ?>
      +data['undelete']) { ?> +
      html('undelete') ?>
      +data['newtalk'] ) { ?> +
      html('newtalk') ?>
      +data['showjumplinks']) { ?> + + + +html('bodytext') ?> + data['catlinks']) { $this->html('catlinks'); } ?> + + data['dataAfterContent']) { $this->html ('dataAfterContent'); } ?> +
      +
      +
      +
      html('userlangattributes') ?>>
      msg('views') ?>
      -
      html('bottomscripts'); /* JS call to runBodyOnloadHook */ ?> html('reporttime') ?> @@ -279,17 +241,23 @@ class ArchLinuxTemplate extends QuickTemplate { data['nav_urls']['recentchangeslinked'] ) { ?>
    • skin->tooltipAndAccesskey('t-recentchangeslinked') ?>>msg('recentchangeslinked') ?>
    • + ?>"skin->tooltipAndAccesskey('t-recentchangeslinked') ?>>msg('recentchangeslinked-toolbox') ?> data['nav_urls']['trackbacklink'])) { ?> + if( isset( $this->data['nav_urls']['trackbacklink'] ) && $this->data['nav_urls']['trackbacklink'] ) { ?> @@ -360,7 +328,7 @@ class ArchLinuxTemplate extends QuickTemplate { if( $this->data['language_urls'] ) { ?>
      -
      msg('otherlanguages') ?>
      + html('userlangattributes') ?>>msg('otherlanguages') ?>
        data['language_urls'] as $langlink) { ?> @@ -378,7 +346,7 @@ class ArchLinuxTemplate extends QuickTemplate { function customBox( $bar, $cont ) { ?>
        skin->tooltip('p-'.$bar) ?>> -
        +
          @@ -397,7 +365,6 @@ class ArchLinuxTemplate extends QuickTemplate {
        skinname = 'chick'; - $this->stylename = 'chick'; - $this->template = 'MonoBookTemplate'; - } + var $skinname = 'chick', $stylename = 'chick', + $template = 'MonoBookTemplate', $useHeadElement = true; function setupSkinUserCss( OutputPage $out ){ parent::setupSkinUserCss( $out ); @@ -34,5 +30,3 @@ class SkinChick extends SkinTemplate { $out->addStyle( 'chick/IE60Fixes.css', 'screen,handheld', 'IE 6' ); } } - - diff --git a/skins/CologneBlue.php b/skins/CologneBlue.php index c650cbee..a7aac8a0 100644 --- a/skins/CologneBlue.php +++ b/skins/CologneBlue.php @@ -1,14 +1,15 @@ qbSetting(); $mainPageObj = Title::newMainPage(); - $s .= "\n
        \n
        " . - ""; + $s = "\n
        \n
        " . + '
        '; - $s .= "
        "; - $s .= "escapeLocalURL() . "\">"; - $s .= "" . wfMsg( "sitetitle" ) . ""; + $s .= ''; + $s .= ''; + $s .= '' . wfMsg( 'sitetitle' ) . ''; - $s .= ""; + $s .= ''; $s .= $this->sysLinks(); - $s .= "
        "; + $s .= '
        '; - $s .= ""; - $s .= htmlspecialchars( wfMsg( "sitesubtitle" ) ) . ""; - $s .= "" ; + $s .= ''; + $s .= htmlspecialchars( wfMsg( 'sitesubtitle' ) ) . ''; + $s .= ''; - $s .= "" ; - $s .= str_replace ( "
        " , "" , $this->otherLanguages() ); + $s .= ''; + $s .= str_replace( '
        ', '', $this->otherLanguages() ); $cat = $this->getCategoryLinks(); - if( $cat ) $s .= "
        $cat\n"; - $s .= "
        " . $this->pageTitleLinks(); - $s .= "
        "; + if( $cat ) { + $s .= "
        $cat\n"; + } + $s .= '
        ' . $this->pageTitleLinks(); + $s .= '
        '; $s .= "
        \n"; @@ -64,37 +66,44 @@ class SkinCologneBlue extends Skin { return $s; } - function doAfterContent() - { - global $wgOut, $wgLang; + function doAfterContent(){ + global $wgLang; $s = "\n

        \n"; $s .= "\n\n
        \n"; - if ( 0 != $qb ) { $s .= $this->quickBar(); } + if ( 0 != $qb ) { + $s .= $this->quickBar(); + } return $s; } @@ -104,42 +113,51 @@ class SkinCologneBlue extends Skin { if ( 2 == $qb ) { # Right $s .= "#quickbar { position: absolute; right: 4px; }\n" . - "#article { margin-left: 4px; margin-right: 148px; }\n"; - } else if ( 1 == $qb ) { + "#article { margin-left: 4px; margin-right: 148px; }\n"; + } elseif ( 1 == $qb ) { $s .= "#quickbar { position: absolute; left: 4px; }\n" . - "#article { margin-left: 148px; margin-right: 4px; }\n"; - } else if ( 3 == $qb ) { # Floating left + "#article { margin-left: 148px; margin-right: 4px; }\n"; + } elseif ( 3 == $qb ) { # Floating left $s .= "#quickbar { position:absolute; left:4px } \n" . - "#topbar { margin-left: 148px }\n" . - "#article { margin-left:148px; margin-right: 4px; } \n" . - "body>#quickbar { position:fixed; left:4px; top:4px; overflow:auto ;bottom:4px;} \n"; # Hides from IE - } else if ( 4 == $qb ) { # Floating right + "#topbar { margin-left: 148px }\n" . + "#article { margin-left:148px; margin-right: 4px; } \n" . + "body>#quickbar { position:fixed; left:4px; top:4px; overflow:auto ;bottom:4px;} \n"; # Hides from IE + } elseif ( 4 == $qb ) { # Floating right $s .= "#quickbar { position: fixed; right: 4px; } \n" . - "#topbar { margin-right: 148px }\n" . - "#article { margin-right: 148px; margin-left: 4px; } \n" . - "body>#quickbar { position: fixed; right: 4px; top: 4px; overflow: auto ;bottom:4px;} \n"; # Hides from IE + "#topbar { margin-right: 148px }\n" . + "#article { margin-right: 148px; margin-left: 4px; } \n" . + "body>#quickbar { position: fixed; right: 4px; top: 4px; overflow: auto ;bottom:4px;} \n"; # Hides from IE } return $s; } function sysLinks() { - global $wgUser, $wgLang, $wgContLang, $wgTitle; - $li = $wgContLang->specialPage("Userlogin"); - $lo = $wgContLang->specialPage("Userlogout"); + global $wgUser, $wgLang, $wgContLang; + $li = SpecialPage::getTitleFor( 'Userlogin' ); + $lo = SpecialPage::getTitleFor( 'Userlogout' ); - $rt = $wgTitle->getPrefixedURL(); + $rt = $this->mTitle->getPrefixedURL(); if ( 0 == strcasecmp( urlencode( $lo ), $rt ) ) { - $q = ""; + $q = array(); } else { - $q = "returnto={$rt}"; + $q = array( 'returnto' => $rt ); } $s = array( $this->mainPageLink(), - $this->makeKnownLink( wfMsgForContent( "aboutpage" ), wfMsg( "about" ) ), - $this->makeKnownLink( wfMsgForContent( "helppage" ), wfMsg( "help" ) ), - $this->makeKnownLink( wfMsgForContent( "faqpage" ), wfMsg("faq") ), - $this->specialLink( "specialpages" ) + $this->linkKnown( + Title::newFromText( wfMsgForContent( 'aboutpage' ) ), + wfMsg( 'about' ) + ), + $this->linkKnown( + Title::newFromText( wfMsgForContent( 'helppage' ) ), + wfMsg( 'help' ) + ), + $this->linkKnown( + Title::newFromText( wfMsgForContent( 'faqpage' ) ), + wfMsg( 'faq' ) + ), + $this->specialLink( 'specialpages' ) ); /* show links to different language variants */ @@ -150,9 +168,19 @@ class SkinCologneBlue extends Skin { $s[] = $this->extensionTabLinks(); } if ( $wgUser->isLoggedIn() ) { - $s[] = $this->makeKnownLink( $lo, wfMsg( "logout" ), $q ); + $s[] = $this->linkKnown( + $lo, + wfMsg( 'logout' ), + array(), + $q + ); } else { - $s[] = $this->makeKnownLink( $li, wfMsg( "login" ), $q ); + $s[] = $this->linkKnown( + $li, + wfMsg( 'login' ), + array(), + $q + ); } return $wgLang->pipeList( $s ); @@ -162,19 +190,18 @@ class SkinCologneBlue extends Skin { * Compute the sidebar * @access private */ - function quickBar() - { - global $wgOut, $wgTitle, $wgUser, $wgLang, $wgContLang, $wgEnableUploads; + function quickBar(){ + global $wgOut, $wgUser, $wgEnableUploads; - $tns=$wgTitle->getNamespace(); + $tns = $this->mTitle->getNamespace(); $s = "\n
        "; - $sep = "
        "; - $s .= $this->menuHead( "qbfind" ); + $sep = '
        '; + $s .= $this->menuHead( 'qbfind' ); $s .= $this->searchForm(); - $s .= $this->menuHead( "qbbrowse" ); + $s .= $this->menuHead( 'qbbrowse' ); # Use the first heading from the Monobook sidebar as the "browse" section $bar = $this->buildSidebar(); @@ -191,46 +218,49 @@ class SkinCologneBlue extends Skin { } if ( $wgOut->isArticle() ) { - $s .= $this->menuHead( "qbedit" ); - $s .= "" . $this->editThisPage() . ""; + $s .= $this->menuHead( 'qbedit' ); + $s .= '' . $this->editThisPage() . ''; - $s .= $sep . $this->makeKnownLink( wfMsgForContent( "edithelppage" ), wfMsg( "edithelp" ) ); + $s .= $sep . $this->linkKnown( + Title::newFromText( wfMsgForContent( 'edithelppage' ) ), + wfMsg( 'edithelp' ) + ); if( $wgUser->isLoggedIn() ) { $s .= $sep . $this->moveThisPage(); } - if ( $wgUser->isAllowed('delete') ) { + if ( $wgUser->isAllowed( 'delete' ) ) { $dtp = $this->deleteThisPage(); - if ( "" != $dtp ) { + if ( $dtp != '' ) { $s .= $sep . $dtp; } } - if ( $wgUser->isAllowed('protect') ) { + if ( $wgUser->isAllowed( 'protect' ) ) { $ptp = $this->protectThisPage(); - if ( "" != $ptp ) { + if ( $ptp != '' ) { $s .= $sep . $ptp; } } $s .= $sep; - $s .= $this->menuHead( "qbpageoptions" ); + $s .= $this->menuHead( 'qbpageoptions' ); $s .= $this->talkLink() - . $sep . $this->commentLink() - . $sep . $this->printableLink(); + . $sep . $this->commentLink() + . $sep . $this->printableLink(); if ( $wgUser->isLoggedIn() ) { $s .= $sep . $this->watchThisPage(); } $s .= $sep; - $s .= $this->menuHead("qbpageinfo") - . $this->historyLink() - . $sep . $this->whatLinksHere() - . $sep . $this->watchPageLinksLink(); + $s .= $this->menuHead( 'qbpageinfo' ) + . $this->historyLink() + . $sep . $this->whatLinksHere() + . $sep . $this->watchPageLinksLink(); if( $tns == NS_USER || $tns == NS_USER_TALK ) { - $id=User::idFromName($wgTitle->getText()); - if ($id != 0) { + $id = User::idFromName( $this->mTitle->getText() ); + if( $id != 0 ) { $s .= $sep . $this->userContribsLink(); if( $this->showEmailUser( $id ) ) { $s .= $sep . $this->emailUserLink(); @@ -240,72 +270,92 @@ class SkinCologneBlue extends Skin { $s .= $sep; } - $s .= $this->menuHead( "qbmyoptions" ); + $s .= $this->menuHead( 'qbmyoptions' ); if ( $wgUser->isLoggedIn() ) { $name = $wgUser->getName(); - $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), - wfMsg( 'mytalk' ) ); + $tl = $this->link( + $wgUser->getTalkPage(), + wfMsg( 'mytalk' ), + array(), + array(), + array( 'known', 'noclasses' ) + ); if ( $wgUser->getNewtalk() ) { - $tl .= " *"; + $tl .= ' *'; } - $s .= $this->makeKnownLinkObj( $wgUser->getUserPage(), - wfMsg( "mypage" ) ) - . $sep . $tl - . $sep . $this->specialLink( "watchlist" ) - . $sep . $this->makeKnownLinkObj( SpecialPage::getSafeTitleFor( "Contributions", $wgUser->getName() ), - wfMsg( "mycontris" ) ) - . $sep . $this->specialLink( "preferences" ) - . $sep . $this->specialLink( "userlogout" ); + $s .= $this->link( + $wgUser->getUserPage(), + wfMsg( 'mypage' ), + array(), + array(), + array( 'known', 'noclasses' ) + ) . $sep . $tl . $sep . $this->specialLink( 'watchlist' ) + . $sep . + $this->link( + SpecialPage::getSafeTitleFor( 'Contributions', $wgUser->getName() ), + wfMsg( 'mycontris' ), + array(), + array(), + array( 'known', 'noclasses' ) + ) . $sep . $this->specialLink( 'preferences' ) + . $sep . $this->specialLink( 'userlogout' ); } else { - $s .= $this->specialLink( "userlogin" ); + $s .= $this->specialLink( 'userlogin' ); } - $s .= $this->menuHead( "qbspecialpages" ) - . $this->specialLink( "newpages" ) - . $sep . $this->specialLink( "listfiles" ) - . $sep . $this->specialLink( "statistics" ); + $s .= $this->menuHead( 'qbspecialpages' ) + . $this->specialLink( 'newpages' ) + . $sep . $this->specialLink( 'listfiles' ) + . $sep . $this->specialLink( 'statistics' ); if ( $wgUser->isLoggedIn() && $wgEnableUploads ) { - $s .= $sep . $this->specialLink( "upload" ); + $s .= $sep . $this->specialLink( 'upload' ); } + global $wgSiteSupportPage; - if( $wgSiteSupportPage) { - $s .= $sep."" - .wfMsg( "sitesupport" ).""; + + if( $wgSiteSupportPage ) { + $s .= $sep . '' + . wfMsg( 'sitesupport' ) . ''; } - $s .= $sep . $this->makeKnownLinkObj( + $s .= $sep . $this->link( SpecialPage::getTitleFor( 'Specialpages' ), - wfMsg( 'moredotdotdot' ) ); + wfMsg( 'moredotdotdot' ), + array(), + array(), + array( 'known', 'noclasses' ) + ); $s .= $sep . "\n
        \n"; return $s; } - function menuHead( $key ) - { + function menuHead( $key ) { $s = "\n
        " . wfMsg( $key ) . "
        "; return $s; } - function searchForm( $label = "" ) - { + function searchForm( $label = '' ) { global $wgRequest, $wgUseTwoButtonsSearchForm; $search = $wgRequest->getText( 'search' ); $action = $this->escapeSearchLink(); $s = "
        searchboxes}\" method=\"get\" class=\"inline\" action=\"$action\">"; - if ( "" != $label ) { $s .= "{$label}: "; } + if( $label != '' ) { + $s .= "{$label}: "; + } $s .= "searchboxes}\" class=\"mw-searchInput\" name=\"search\" size=\"14\" value=\"" - . htmlspecialchars(substr($search,0,256)) . "\" />
        " - . "searchboxes}\" class=\"searchButton\" name=\"go\" value=\"" . htmlspecialchars( wfMsg( "searcharticle" ) ) . "\" />"; - - if ($wgUseTwoButtonsSearchForm) - $s .= "searchboxes}\" class=\"searchButton\" name=\"fulltext\" value=\"" . htmlspecialchars( wfMsg( "search" ) ) . "\" />\n"; - else - $s .= '\n"; - + . htmlspecialchars( substr( $search, 0, 256 ) ) . "\" />
        " + . "searchboxes}\" class=\"searchButton\" name=\"go\" value=\"" . htmlspecialchars( wfMsg( 'searcharticle' ) ) . "\" />"; + + if( $wgUseTwoButtonsSearchForm ) { + $s .= "searchboxes}\" class=\"searchButton\" name=\"fulltext\" value=\"" . htmlspecialchars( wfMsg( 'search' ) ) . "\" />\n"; + } else { + $s .= '\n"; + } + $s .= '
        '; // Ensure unique id's for search boxes made after the first @@ -314,5 +364,3 @@ class SkinCologneBlue extends Skin { return $s; } } - - diff --git a/skins/Modern.php b/skins/Modern.php index 6b6651d1..ef1b1a1e 100644 --- a/skins/Modern.php +++ b/skins/Modern.php @@ -16,8 +16,11 @@ if( !defined( 'MEDIAWIKI' ) ) * @ingroup Skins */ class SkinModern extends SkinTemplate { + var $skinname = 'modern', $stylename = 'modern', + $template = 'ModernTemplate', $useHeadElement = true; + /* - * We don't like the default getPoweredBy, the icon clashes with the + * We don't like the default getPoweredBy, the icon clashes with the * skin L&F. */ function getPoweredBy() { @@ -25,14 +28,9 @@ class SkinModern extends SkinTemplate { return "
        Powered by MediaWiki $wgVersion
        "; } - function initPage( OutputPage $out ) { - parent::initPage( $out ); - $this->skinname = 'modern'; - $this->stylename = 'modern'; - $this->template = 'ModernTemplate'; - } - function setupSkinUserCss( OutputPage $out ){ + global $wgStyleVersion, $wgJsMimeType, $wgStylePath; + // Do not call parent::setupSkinUserCss(), we have our own print style $out->addStyle( 'common/shared.css', 'screen' ); $out->addStyle( 'modern/main.css', 'screen' ); @@ -56,53 +54,18 @@ class ModernTemplate extends QuickTemplate { * @access private */ function execute() { - global $wgRequest; + global $wgRequest, $wgOut; $this->skin = $skin = $this->data['skin']; $action = $wgRequest->getText( 'action' ); // Suppress warnings to prevent notices about missing indexes in $this->data wfSuppressWarnings(); -?> -data['xhtmlnamespaces'] as $tag => $ns) { - ?>xmlns:xml:lang="text('lang') ?>" lang="text('lang') ?>" dir="text('dir') ?>"> - - - html('headlinks') ?> - <?php $this->text('pagetitle') ?> - html('csslinks') ?> - - - data ); ?> - - - -html('headscripts') ?> -data['jsvarurl' ]) { ?> - - -data['pagecss' ]) { ?> - -data['usercss' ]) { ?> - -data['userjs' ]) { ?> - -data['userjsprev']) { ?> - -data['trackbackhtml']) print $this->data['trackbackhtml']; ?> - -data['body_ondblclick']) { ?> ondblclick="text('body_ondblclick') ?>" -data['body_onload' ]) { ?> onload="text('body_onload') ?>" - class="mediawiki text('dir') ?> text('pageclass') ?> text('skinnameclass') ?>"> + $this->html( 'headelement' ); +?> -

        data['displaytitle']!=""?$this->html('title'):$this->text('title') ?>

        +

        html('title') ?>

        @@ -141,9 +104,9 @@ class ModernTemplate extends QuickTemplate { -
        +
        html("specialpageattributes") ?>>
        - +
        msg('tagline') ?>
        data['newtalk'] ) { ?>
        html('newtalk') ?>
        @@ -153,7 +116,7 @@ class ModernTemplate extends QuickTemplate {
        -
        html('subtitle') ?>
        +
        html('userlangattributes') ?>>html('subtitle') ?>
        data['undelete']) { ?>
        html('undelete') ?>
        data['showjumplinks']) { ?> @@ -166,11 +129,11 @@ class ModernTemplate extends QuickTemplate {
        -
        +
        html("userlangattributes") ?>> - data['sidebar']; + data['sidebar']; if ( !isset( $sidebar['SEARCH'] ) ) $sidebar['SEARCH'] = true; if ( !isset( $sidebar['TOOLBOX'] ) ) $sidebar['TOOLBOX'] = true; if ( !isset( $sidebar['LANGUAGES'] ) ) $sidebar['LANGUAGES'] = true; @@ -213,8 +176,8 @@ class ModernTemplate extends QuickTemplate {
        - - +
        data['nav_urls']['recentchangeslinked'] ) { ?>
      • skin->tooltipAndAccesskey('t-recentchangeslinked') ?>>msg('recentchangeslinked') ?>
      • + ?>"skin->tooltipAndAccesskey('t-recentchangeslinked') ?>>msg('recentchangeslinked-toolbox') ?> data['nav_urls']['trackbacklink'])) { ?> @@ -320,18 +283,17 @@ class ModernTemplate extends QuickTemplate { } wfRunHooks( 'SkinTemplateToolboxEnd', array( &$this ) ); -?>
      -
      -
      +?> +
    + + data['language_urls'] ) { ?> - -data['language_urls'] ) { ?>
    msg('otherlanguages') ?>
    @@ -341,8 +303,8 @@ class ModernTemplate extends QuickTemplate { ?> -
    -
    + + - - + + + + diff --git a/skins/MonoBook.php b/skins/MonoBook.php index 2312de0f..fdc1684d 100644 --- a/skins/MonoBook.php +++ b/skins/MonoBook.php @@ -20,13 +20,8 @@ if( !defined( 'MEDIAWIKI' ) ) */ class SkinMonoBook extends SkinTemplate { /** Using monobook. */ - function initPage( OutputPage $out ) { - parent::initPage( $out ); - $this->skinname = 'monobook'; - $this->stylename = 'monobook'; - $this->template = 'MonoBookTemplate'; - - } + var $skinname = 'monobook', $stylename = 'monobook', + $template = 'MonoBookTemplate', $useHeadElement = true; function setupSkinUserCss( OutputPage $out ) { global $wgHandheldStyle; @@ -46,6 +41,7 @@ class SkinMonoBook extends SkinTemplate { $out->addStyle( 'monobook/IE70Fixes.css', 'screen', 'IE 7' ); $out->addStyle( 'monobook/rtl.css', 'screen', '', 'rtl' ); + } } @@ -65,84 +61,50 @@ class MonoBookTemplate extends QuickTemplate { */ function execute() { global $wgRequest; + $this->skin = $skin = $this->data['skin']; $action = $wgRequest->getText( 'action' ); // Suppress warnings to prevent notices about missing indexes in $this->data wfSuppressWarnings(); -?> -data['xhtmlnamespaces'] as $tag => $ns) { - ?>xmlns:xml:lang="text('lang') ?>" lang="text('lang') ?>" dir="text('dir') ?>"> - - - html('headlinks') ?> - <?php $this->text('pagetitle') ?> - html('csslinks') ?> - - - - data ); ?> + $this->html( 'headelement' ); +?>
    +
    html("specialpageattributes") ?>> + + data['sitenotice']) { ?>
    html('sitenotice') ?>
    - - -html('headscripts') ?> -data['jsvarurl']) { ?> - - -data['pagecss']) { ?> - -data['usercss']) { ?> - -data['userjs']) { ?> - -data['userjsprev']) { ?> - -data['trackbackhtml']) print $this->data['trackbackhtml']; ?> - -data['body_ondblclick']) { ?> ondblclick="text('body_ondblclick') ?>" -data['body_onload']) { ?> onload="text('body_onload') ?>" - class="mediawiki text('dir') ?> text('pageclass') ?> text('skinnameclass') ?>"> -
    -
    -
    - - data['sitenotice']) { ?>
    html('sitenotice') ?>
    -

    data['displaytitle']!=""?$this->html('title'):$this->text('title') ?>

    -
    -

    msg('tagline') ?>

    -
    html('subtitle') ?>
    - data['undelete']) { ?>
    html('undelete') ?>
    - data['newtalk'] ) { ?>
    html('newtalk') ?>
    - data['showjumplinks']) { ?> - - html('bodytext') ?> - data['catlinks']) { $this->html('catlinks'); } ?> - - data['dataAfterContent']) { $this->html ('dataAfterContent'); } ?> -
    -
    +

    html('title') ?>

    +
    +

    msg('tagline') ?>

    +
    html('userlangattributes') ?>>html('subtitle') ?>
    +data['undelete']) { ?> +
    html('undelete') ?>
    +data['newtalk'] ) { ?> +
    html('newtalk') ?>
    +data['showjumplinks']) { ?> + + + +html('bodytext') ?> + data['catlinks']) { $this->html('catlinks'); } ?> + + data['dataAfterContent']) { $this->html ('dataAfterContent'); } ?> +
    -
    -
    +
    +
    html('userlangattributes') ?>>
    msg('views') ?>
    -
    html('bottomscripts'); /* JS call to runBodyOnloadHook */ ?> html('reporttime') ?> @@ -254,17 +217,23 @@ class MonoBookTemplate extends QuickTemplate { data['nav_urls']['recentchangeslinked'] ) { ?>
  • skin->tooltipAndAccesskey('t-recentchangeslinked') ?>>msg('recentchangeslinked') ?>
  • + ?>"skin->tooltipAndAccesskey('t-recentchangeslinked') ?>>msg('recentchangeslinked-toolbox') ?> data['nav_urls']['trackbacklink'])) { ?> + if( isset( $this->data['nav_urls']['trackbacklink'] ) && $this->data['nav_urls']['trackbacklink'] ) { ?> @@ -335,7 +304,7 @@ class MonoBookTemplate extends QuickTemplate { if( $this->data['language_urls'] ) { ?>
    -
    msg('otherlanguages') ?>
    + html('userlangattributes') ?>>msg('otherlanguages') ?>
      data['language_urls'] as $langlink) { ?> @@ -353,7 +322,7 @@ class MonoBookTemplate extends QuickTemplate { function customBox( $bar, $cont ) { ?>
      skin->tooltip('p-'.$bar) ?>> -
      +
        @@ -372,7 +341,6 @@ class MonoBookTemplate extends QuickTemplate {
      skinname = 'myskin'; - $this->stylename = 'myskin'; - $this->template = 'MonoBookTemplate'; - } + var $skinname = 'myskin', $stylename = 'myskin', + $template = 'MonoBookTemplate', $useHeadElement = true; } diff --git a/skins/Nostalgia.php b/skins/Nostalgia.php index d4dee0f4..d4f3f06f 100644 --- a/skins/Nostalgia.php +++ b/skins/Nostalgia.php @@ -1,14 +1,14 @@ \n
      \n"; - $s .= "
      ".$this->logoText( "right" )."
      "; + $s .= ''; $s .= $this->pageTitle(); $s .= $this->pageSubtitle() . "\n"; - $s .= "
      "; + $s .= '
      '; $s .= $this->topLinks() . "\n
      "; $notice = wfGetSiteNotice(); @@ -40,10 +41,14 @@ class SkinNostalgia extends Skin { $s .= $this->pageTitleLinks(); $ol = $this->otherLanguages(); - if($ol) $s .= "
      " . $ol; + if( $ol ) { + $s .= '
      ' . $ol; + } $cat = $this->getCategoryLinks(); - if($cat) $s .= "
      " . $cat; + if( $cat ) { + $s .= '
      ' . $cat; + } $s .= "

      \n
      \n"; $s .= "\n
      "; @@ -79,7 +84,7 @@ class SkinNostalgia extends Skin { $s .= $sep . $this->specialLink( 'watchlist' ); /* show my contributions link */ $s .= $sep . $this->link( - SpecialPage::getSafeTitleFor( "Contributions", $wgUser->getName() ), + SpecialPage::getSafeTitleFor( 'Contributions', $wgUser->getName() ), wfMsgHtml( 'mycontris' ) ); /* show my preferences link */ $s .= $sep . $this->specialLink( 'preferences' ); @@ -104,13 +109,11 @@ class SkinNostalgia extends Skin { $s .= $this->bottomLinks(); $s .= "\n
      " . $this->pageStats(); $s .= "\n
      " . $this->mainPageLink() - . " | " . $this->aboutLink() - . " | " . $this->searchForm(); + . ' | ' . $this->aboutLink() + . ' | ' . $this->searchForm(); $s .= "\n
      \n
      \n"; return $s; } } - - diff --git a/skins/Simple.php b/skins/Simple.php index b26f50d0..416dc3f6 100644 --- a/skins/Simple.php +++ b/skins/Simple.php @@ -1,8 +1,8 @@ skinname = 'simple'; - $this->stylename = 'simple'; - $this->template = 'MonoBookTemplate'; - } + var $skinname = 'simple', $stylename = 'simple', + $template = 'MonoBookTemplate', $useHeadElement = true; function setupSkinUserCss( OutputPage $out ){ $out->addStyle( 'simple/main.css', 'screen' ); $out->addStyle( 'simple/rtl.css', '', '', 'rtl' ); - } 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')) { + if( $wgUser->getOption( 'highlightbroken' ) ) { $s .= "a.new, #quickbar a.new { text-decoration: line-through; }\n"; } else { - $s .= <<getOption('justify')) { + if( $wgUser->getOption( 'justify' ) ) { $s .= "#article, #bodyContent { text-align: justify; }\n"; } - if (!$wgUser->getOption('showtoc')) { + if( !$wgUser->getOption( 'showtoc' ) ) { $s .= "#toc { display: none; }\n"; } - if (!$wgUser->getOption('editsection')) { + if( !$wgUser->getOption( 'editsection' ) ) { $s .= ".editsection { display: none; }\n"; } return $s; } } - - diff --git a/skins/Skin.sample b/skins/Skin.sample deleted file mode 100644 index c011c143..00000000 --- a/skins/Skin.sample +++ /dev/null @@ -1,19 +0,0 @@ - diff --git a/skins/Standard.php b/skins/Standard.php index 694bc5e3..e57cfaf9 100644 --- a/skins/Standard.php +++ b/skins/Standard.php @@ -1,14 +1,14 @@ qbSetting() ) { # Floating left - $s .= "\n"; - } - return $s; - } - /** * */ function setupSkinUserCss( OutputPage $out ){ if ( 3 == $this->qbSetting() ) { # Floating left $out->addStyle( 'common/quickbar.css' ); - } else if ( 4 == $this->qbSetting() ) { # Floating right + } elseif ( 4 == $this->qbSetting() ) { # Floating right $out->addStyle( 'common/quickbar-right.css' ); } parent::setupSkinUserCss( $out ); @@ -51,60 +37,44 @@ class SkinStandard extends Skin { if ( 2 == $qb ) { # Right $s .= "#quickbar { position: absolute; top: 4px; right: 4px; " . - "border-left: 2px solid #000000; }\n" . - "#article, #mw-data-after-content { margin-left: 4px; margin-right: 152px; }\n"; - } else if ( 1 == $qb || 3 == $qb ) { + "border-left: 2px solid #000000; }\n" . + "#article, #mw-data-after-content { margin-left: 4px; margin-right: 152px; }\n"; + } elseif ( 1 == $qb || 3 == $qb ) { $s .= "#quickbar { position: absolute; top: 4px; left: 4px; " . - "border-right: 1px solid gray; }\n" . - "#article, #mw-data-after-content { margin-left: 152px; margin-right: 4px; }\n"; - } else if ( 4 == $qb) { + "border-right: 1px solid gray; }\n" . + "#article, #mw-data-after-content { margin-left: 152px; margin-right: 4px; }\n"; + } elseif ( 4 == $qb ) { $s .= "#quickbar { border-right: 1px solid gray; }\n" . - "#article, #mw-data-after-content { margin-right: 152px; margin-left: 4px; }\n"; + "#article, #mw-data-after-content { margin-right: 152px; margin-left: 4px; }\n"; } return $s; } - /** - * - */ - function getBodyOptions() { - $a = parent::getBodyOptions(); - - if ( 3 == $this->qbSetting() ) { # Floating left - $qb = "setup(\"quickbar\")"; - if($a["onload"]) { - $a["onload"] .= ";$qb"; - } else { - $a["onload"] = $qb; - } - } - return $a; - } - function doAfterContent() { global $wgContLang, $wgLang; - $fname = 'SkinStandard::doAfterContent'; - wfProfileIn( $fname ); - wfProfileIn( $fname.'-1' ); + wfProfileIn( __METHOD__ ); + wfProfileIn( __METHOD__ . '-1' ); $s = "\n

      \n"; $s .= "\n\n
    \n"; - wfProfileOut( $fname.'-3' ); - wfProfileIn( $fname.'-4' ); - if ( 0 != $qb ) { $s .= $this->quickBar(); } - wfProfileOut( $fname.'-4' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ . '-3' ); + wfProfileIn( __METHOD__ . '-4' ); + if ( 0 != $qb ) { + $s .= $this->quickBar(); + } + wfProfileOut( __METHOD__ . '-4' ); + wfProfileOut( __METHOD__ ); return $s; } function quickBar() { - global $wgOut, $wgTitle, $wgUser, $wgRequest, $wgContLang; + global $wgOut, $wgUser, $wgRequest, $wgContLang; global $wgEnableUploads, $wgRemoteUploads; - $fname = 'Skin::quickBar'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); $action = $wgRequest->getText( 'action' ); $wpPreview = $wgRequest->getBool( 'wpPreview' ); - $tns=$wgTitle->getNamespace(); + $tns = $this->mTitle->getNamespace(); $s = "\n
    "; $s .= "\n" . $this->logoText() . "\n
    "; @@ -161,18 +132,22 @@ class SkinStandard extends Skin { if( $wgUser->isLoggedIn() ) { $s.= $this->specialLink( 'watchlist' ) ; - $s .= $sep . $this->makeKnownLink( $wgContLang->specialPage( 'Contributions' ), - wfMsg( 'mycontris' ), 'target=' . wfUrlencode($wgUser->getName() ) ); + $s .= $sep . $this->linkKnown( + SpecialPage::getTitleFor( 'Contributions' ), + wfMsg( 'mycontris' ), + array(), + array( 'target' => $wgUser->getName() ) + ); } // only show watchlist link if logged in $s .= "\n
    "; - $articleExists = $wgTitle->getArticleId(); - if ( $wgOut->isArticle() || $action =='edit' || $action =='history' || $wpPreview) { - if($wgOut->isArticle()) { + $articleExists = $this->mTitle->getArticleId(); + if ( $wgOut->isArticle() || $action == 'edit' || $action == 'history' || $wpPreview ) { + if( $wgOut->isArticle() ) { $s .= '' . $this->editThisPage() . ''; } else { # backlink to the article in edit or history mode - if($articleExists){ # no backlink if no article - switch($tns) { + if( $articleExists ){ # no backlink if no article + switch( $tns ) { case NS_TALK: case NS_USER_TALK: case NS_PROJECT_TALK: @@ -208,29 +183,40 @@ class SkinStandard extends Skin { $text = wfMsg( 'categorypage' ); break; default: - $text= wfMsg( 'articlepage' ); + $text = wfMsg( 'articlepage' ); } - $link = $wgTitle->getText(); - if ($nstext = $wgContLang->getNsText($tns) ) { # add namespace if necessary - $link = $nstext . ':' . $link ; + $link = $this->mTitle->getText(); + if( $nstext = $wgContLang->getNsText( $tns ) ) { # add namespace if necessary + $link = $nstext . ':' . $link; } - $s .= $this->makeLink( $link, $text ); - } elseif( $wgTitle->getNamespace() != NS_SPECIAL ) { + $s .= $this->link( + Title::newFromText( $link ), + $text + ); + } elseif( $this->mTitle->getNamespace() != NS_SPECIAL ) { # we just throw in a "New page" text to tell the user that he's in edit mode, # and to avoid messing with the separator that is prepended to the next item - $s .= '' . wfMsg('newpage') . ''; + $s .= '' . wfMsg( 'newpage' ) . ''; } - } # "Post a comment" link - if( ( $wgTitle->isTalkPage() || $wgOut->showNewSectionLink() ) && $action != 'edit' && !$wpPreview ) - $s .= '
    ' . $this->makeKnownLinkObj( $wgTitle, wfMsg( 'postcomment' ), 'action=edit§ion=new' ); - + if( ( $this->mTitle->isTalkPage() || $wgOut->showNewSectionLink() ) && $action != 'edit' && !$wpPreview ) + $s .= '
    ' . $this->link( + $this->mTitle, + wfMsg( 'postcomment' ), + array(), + array( + 'action' => 'edit', + 'section' => 'new' + ), + array( 'known', 'noclasses' ) + ); + #if( $tns%2 && $action!='edit' && !$wpPreview) { - #$s.= '
    '.$this->makeKnownLink($wgTitle->getPrefixedText(),wfMsg('postcomment'),'action=edit§ion=new'); + #$s.= '
    '.$this->linkKnown( Title::newFromText( $wgTitle->getPrefixedText() ),wfMsg('postcomment'),array(),array('action'=>'edit','section'=>'new')); #} /* @@ -239,33 +225,34 @@ class SkinStandard extends Skin { article with "Watch this article" checkbox disabled, the article is transparently unwatched. Therefore we do not show the "Watch this page" link in edit mode */ - if ( $wgUser->isLoggedIn() && $articleExists) { - if($action!='edit' && $action != 'submit' ) - { + if ( $wgUser->isLoggedIn() && $articleExists ) { + if( $action != 'edit' && $action != 'submit' ) { $s .= $sep . $this->watchThisPage(); } - if ( $wgTitle->userCan( 'edit' ) ) + if ( $this->mTitle->userCan( 'edit' ) ) $s .= $sep . $this->moveThisPage(); } - if ( $wgUser->isAllowed('delete') and $articleExists ) { + if ( $wgUser->isAllowed( 'delete' ) && $articleExists ) { $s .= $sep . $this->deleteThisPage() . $sep . $this->protectThisPage(); } $s .= $sep . $this->talkLink(); - if ($articleExists && $action !='history') { + if( $articleExists && $action != 'history' ) { $s .= $sep . $this->historyLink(); } - $s.=$sep . $this->whatLinksHere(); + $s .= $sep . $this->whatLinksHere(); - if($wgOut->isArticleRelated()) { + if( $wgOut->isArticleRelated() ) { $s .= $sep . $this->watchPageLinksLink(); } - if ( NS_USER == $wgTitle->getNamespace() - || $wgTitle->getNamespace() == NS_USER_TALK ) { + if ( + NS_USER == $this->mTitle->getNamespace() || + $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 ){ $s .= $sep . $this->userContribsLink(); @@ -289,9 +276,8 @@ class SkinStandard extends Skin { } $s .= "\n
    \n"; - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return $s; } - } diff --git a/skins/Vector.deps.php b/skins/Vector.deps.php new file mode 100644 index 00000000..7a8c2881 --- /dev/null +++ b/skins/Vector.deps.php @@ -0,0 +1,11 @@ +addStyle( 'vector/main-rtl.css', 'screen', '', 'rtl' ); + $out->addStyle( 'vector/main-ltr.css', 'screen', '', 'ltr' ); + // Append CSS which includes IE only behavior fixes for hover support - + // this is better than including this in a CSS fille since it doesn't + // wait for the CSS file to load before fetching the HTC file. + $out->addScript( + '' + ); + // Add extra stylesheets + // THIS IS ONLY USEFUL FOR EXPERIMENTING WITH DIFFERNT STYLE OPTIONS! THIS WILL BE REMOVED IN THE NEAR FUTURE. + if ( is_array( $wgVectorExtraStyles ) ) { + foreach ( $wgVectorExtraStyles as $style ) { + $out->addStyle( 'vector/' . $style, 'screen' ); + } + } + } + /** + * Builds a structured array of links used for tabs and menus + * @return array + * @private + */ + function buildNavigationUrls() { + global $wgContLang, $wgLang, $wgOut, $wgUser, $wgRequest, $wgArticle, $wgStylePath; + global $wgDisableLangConversion, $wgVectorUseIconWatch; + + wfProfileIn( __METHOD__ ); + + $links = array( + 'namespaces' => array(), + 'views' => array(), + 'actions' => array(), + 'variants' => array() + ); + + // Detects parameters + $action = $wgRequest->getVal( 'action', 'view' ); + $section = $wgRequest->getVal( 'section' ); + + // Checks if page is some kind of content + if( $this->iscontent ) { + + // Gets page objects for the related namespaces + $subjectPage = $this->mTitle->getSubjectPage(); + $talkPage = $this->mTitle->getTalkPage(); + + // Determines if this is a talk page + $isTalk = $this->mTitle->isTalkPage(); + + // Generates XML IDs from namespace names + $subjectId = $this->mTitle->getNamespaceKey( '' ); + + if ( $subjectId == 'main' ) { + $talkId = 'talk'; + } else { + $talkId = "{$subjectId}_talk"; + } + $currentId = $isTalk ? $talkId : $subjectId; + + // Adds namespace links + $links['namespaces'][$subjectId] = $this->tabAction( + $subjectPage, 'vector-namespace-' . $subjectId, !$isTalk, '', true + ); + $links['namespaces'][$subjectId]['context'] = 'subject'; + $links['namespaces'][$talkId] = $this->tabAction( + $talkPage, 'vector-namespace-talk', $isTalk, '', true + ); + $links['namespaces'][$talkId]['context'] = 'talk'; + + // Adds view view link + if ( $this->mTitle->exists() ) { + $links['views']['view'] = $this->tabAction( + $isTalk ? $talkPage : $subjectPage, + 'vector-view-view', ( $action == 'view' ), '', true + ); + } + + wfProfileIn( __METHOD__ . '-edit' ); + + // Checks if user can... + if ( + // edit the current page + $this->mTitle->quickUserCan( 'edit' ) && + ( + // if it exists + $this->mTitle->exists() || + // or they can create one here + $this->mTitle->quickUserCan( 'create' ) + ) + ) { + // Builds CSS class for talk page links + $isTalkClass = $isTalk ? ' istalk' : ''; + + // Determines if we're in edit mode + $selected = ( + ( $action == 'edit' || $action == 'submit' ) && + ( $section != 'new' ) + ); + $links['views']['edit'] = array( + 'class' => ( $selected ? 'selected' : '' ) . $isTalkClass, + 'text' => $this->mTitle->exists() + ? wfMsg( 'vector-view-edit' ) + : wfMsg( 'vector-view-create' ), + 'href' => + $this->mTitle->getLocalUrl( $this->editUrlOptions() ) + ); + // Checks if this is a current rev of talk page and we should show a new + // section link + if ( ( $isTalk && $wgArticle->isCurrent() ) || ( $wgOut->showNewSectionLink() ) ) { + // Checks if we should ever show a new section link + if ( !$wgOut->forceHideNewSectionLink() ) { + // Adds new section link + //$links['actions']['addsection'] + $links['views']['addsection'] = array( + 'class' => 'collapsible ' . ( $section == 'new' ? 'selected' : false ), + 'text' => wfMsg( 'vector-action-addsection' ), + 'href' => $this->mTitle->getLocalUrl( + 'action=edit§ion=new' + ) + ); + } + } + // Checks if the page is known (some kind of viewable content) + } elseif ( $this->mTitle->isKnown() ) { + // Adds view source view link + $links['views']['viewsource'] = array( + 'class' => ( $action == 'edit' ) ? 'selected' : false, + 'text' => wfMsg( 'vector-view-viewsource' ), + 'href' => + $this->mTitle->getLocalUrl( $this->editUrlOptions() ) + ); + } + wfProfileOut( __METHOD__ . '-edit' ); + + wfProfileIn( __METHOD__ . '-live' ); + + // Checks if the page exists + if ( $this->mTitle->exists() ) { + // Adds history view link + $links['views']['history'] = array( + 'class' => 'collapsible ' . ( ($action == 'history') ? 'selected' : false ), + 'text' => wfMsg( 'vector-view-history' ), + 'href' => $this->mTitle->getLocalUrl( 'action=history' ), + 'rel' => 'archives', + ); + + if( $wgUser->isAllowed( 'delete' ) ) { + $links['actions']['delete'] = array( + 'class' => ($action == 'delete') ? 'selected' : false, + 'text' => wfMsg( 'vector-action-delete' ), + 'href' => $this->mTitle->getLocalUrl( 'action=delete' ) + ); + } + if ( $this->mTitle->quickUserCan( 'move' ) ) { + $moveTitle = SpecialPage::getTitleFor( + 'Movepage', $this->thispage + ); + $links['actions']['move'] = array( + 'class' => $this->mTitle->isSpecial( 'Movepage' ) ? + 'selected' : false, + 'text' => wfMsg( 'vector-action-move' ), + 'href' => $moveTitle->getLocalUrl() + ); + } + + if ( + $this->mTitle->getNamespace() !== NS_MEDIAWIKI && + $wgUser->isAllowed( 'protect' ) + ) { + if ( !$this->mTitle->isProtected() ){ + $links['actions']['protect'] = array( + 'class' => ($action == 'protect') ? + 'selected' : false, + 'text' => wfMsg( 'vector-action-protect' ), + 'href' => + $this->mTitle->getLocalUrl( 'action=protect' ) + ); + + } else { + $links['actions']['unprotect'] = array( + 'class' => ($action == 'unprotect') ? + 'selected' : false, + 'text' => wfMsg( 'vector-action-unprotect' ), + 'href' => + $this->mTitle->getLocalUrl( 'action=unprotect' ) + ); + } + } + } else { + // article doesn't exist or is deleted + if ( + $wgUser->isAllowed( 'deletedhistory' ) && + $wgUser->isAllowed( 'undelete' ) + ) { + if( $n = $this->mTitle->isDeleted() ) { + $undelTitle = SpecialPage::getTitleFor( 'Undelete' ); + $links['actions']['undelete'] = array( + 'class' => false, + 'text' => wfMsgExt( + 'vector-action-undelete', + array( 'parsemag' ), + $wgLang->formatNum( $n ) + ), + 'href' => $undelTitle->getLocalUrl( + 'target=' . urlencode( $this->thispage ) + ) + ); + } + } + + if ( + $this->mTitle->getNamespace() !== NS_MEDIAWIKI && + $wgUser->isAllowed( 'protect' ) + ) { + if ( !$this->mTitle->getRestrictions( 'create' ) ) { + $links['actions']['protect'] = array( + 'class' => ($action == 'protect') ? + 'selected' : false, + 'text' => wfMsg( 'vector-action-protect' ), + 'href' => + $this->mTitle->getLocalUrl( 'action=protect' ) + ); + + } else { + $links['actions']['unprotect'] = array( + 'class' => ($action == 'unprotect') ? + 'selected' : false, + 'text' => wfMsg( 'vector-action-unprotect' ), + 'href' => + $this->mTitle->getLocalUrl( 'action=unprotect' ) + ); + } + } + } + wfProfileOut( __METHOD__ . '-live' ); + /** + * The following actions use messages which, if made particular to + * the Vector skin, would break the Ajax code which makes this + * action happen entirely inline. Skin::makeGlobalVariablesScript + * defines a set of messages in a javascript object - and these + * messages are assumed to be global for all skins. Without making + * a change to that procedure these messages will have to remain as + * the global versions. + */ + // Checks if the user is logged in + if ( $this->loggedin ) { + if ( $wgVectorUseIconWatch ) { + $class = 'icon '; + $place = 'views'; + } else { + $class = ''; + $place = 'actions'; + } + $mode = $this->mTitle->userIsWatching() ? 'unwatch' : 'watch'; + $links[$place][$mode] = array( + 'class' => $class . ( ( $action == 'watch' || $action == 'unwatch' ) ? ' selected' : false ), + 'text' => wfMsg( $mode ), // uses 'watch' or 'unwatch' message + 'href' => $this->mTitle->getLocalUrl( 'action=' . $mode ) + ); + } + // This is instead of SkinTemplateTabs - which uses a flat array + wfRunHooks( 'SkinTemplateNavigation', array( &$this, &$links ) ); + + // If it's not content, it's got to be a special page + } else { + $links['namespaces']['special'] = array( + 'class' => 'selected', + 'text' => wfMsg( 'vector-namespace-special' ), + 'href' => $wgRequest->getRequestURL() + ); + } + + // Gets list of language variants + $variants = $wgContLang->getVariants(); + // Checks that language conversion is enabled and variants exist + if( !$wgDisableLangConversion && count( $variants ) > 1 ) { + // Gets preferred variant + $preferred = $wgContLang->getPreferredVariant(); + // Loops over each variant + foreach( $variants as $code ) { + // Gets variant name from language code + $varname = $wgContLang->getVariantname( $code ); + // Checks if the variant is marked as disabled + if( $varname == 'disable' ) { + // Skips this variant + continue; + } + // Appends variant link + $links['variants'][] = array( + 'class' => ( $code == $preferred ) ? 'selected' : false, + 'text' => $varname, + 'href' => $this->mTitle->getLocalURL( '', $code ) + ); + } + } + + wfProfileOut( __METHOD__ ); + + return $links; + } +} + +/** + * QuickTemplate class for Vector skin + * @ingroup Skins + */ +class VectorTemplate extends QuickTemplate { + + /* Members */ + + /** + * @var Cached skin object + */ + var $skin; + + /* Functions */ + + /** + * Outputs the entire contents of the XHTML page + */ + public function execute() { + global $wgRequest, $wgOut, $wgContLang; + + $this->skin = $this->data['skin']; + $action = $wgRequest->getText( 'action' ); + + // Build additional attributes for navigation urls + $nav = $this->skin->buildNavigationUrls(); + foreach ( $nav as $section => $links ) { + foreach ( $links as $key => $link ) { + $xmlID = $key; + if ( isset( $link['context'] ) && $link['context'] == 'subject' ) { + $xmlID = 'ca-nstab-' . $xmlID; + } else if ( isset( $link['context'] ) && $link['context'] == 'talk' ) { + $xmlID = 'ca-talk'; + } else { + $xmlID = 'ca-' . $xmlID; + } + $nav[$section][$key]['attributes'] = + ' id="' . Sanitizer::escapeId( $xmlID ) . '"'; + if ( $nav[$section][$key]['class'] ) { + $nav[$section][$key]['attributes'] .= + ' class="' . htmlspecialchars( $link['class'] ) . '"'; + unset( $nav[$section][$key]['class'] ); + } + // We don't want to give the watch tab an accesskey if the page + // is being edited, because that conflicts with the accesskey on + // the watch checkbox. We also don't want to give the edit tab + // an accesskey, because that's fairly superfluous and conflicts + // with an accesskey (Ctrl-E) often used for editing in Safari. + if ( + in_array( $action, array( 'edit', 'submit' ) ) && + in_array( $key, array( 'edit', 'watch', 'unwatch' ) ) + ) { + $nav[$section][$key]['key'] = + $this->skin->tooltip( $xmlID ); + } else { + $nav[$section][$key]['key'] = + $this->skin->tooltipAndAccesskey( $xmlID ); + } + } + } + $this->data['namespace_urls'] = $nav['namespaces']; + $this->data['view_urls'] = $nav['views']; + $this->data['action_urls'] = $nav['actions']; + $this->data['variant_urls'] = $nav['variants']; + // Build additional attributes for personal_urls + foreach ( $this->data['personal_urls'] as $key => $item) { + $this->data['personal_urls'][$key]['attributes'] = + ' id="' . Sanitizer::escapeId( "pt-$key" ) . '"'; + if ( isset( $item['active'] ) && $item['active'] ) { + $this->data['personal_urls'][$key]['attributes'] .= + ' class="active"'; + } + $this->data['personal_urls'][$key]['key'] = + $this->skin->tooltipAndAccesskey('pt-'.$key); + } + + // Generate additional footer links + $footerlinks = array( + 'info' => array( + 'lastmod', + 'viewcount', + 'numberofwatchingusers', + 'credits', + 'copyright', + 'tagline', + ), + 'places' => array( + 'privacy', + 'about', + 'disclaimer', + ), + ); + // Reduce footer links down to only those which are being used + $validFooterLinks = array(); + foreach( $footerlinks as $category => $links ) { + $validFooterLinks[$category] = array(); + foreach( $links as $link ) { + if( isset( $this->data[$link] ) && $this->data[$link] ) { + $validFooterLinks[$category][] = $link; + } + } + } + // Reverse horizontally rendered navigation elements + if ( $wgContLang->isRTL() ) { + $this->data['view_urls'] = + array_reverse( $this->data['view_urls'] ); + $this->data['namespace_urls'] = + array_reverse( $this->data['namespace_urls'] ); + $this->data['personal_urls'] = + array_reverse( $this->data['personal_urls'] ); + } + // Output HTML Page + $this->html( 'headelement' ); +?> +
    +
    + +
    html('specialpageattributes') ?>> + + + data['sitenotice'] ): ?> + +
    html( 'sitenotice' ) ?>
    + + + +

    html( 'title' ) ?>

    + + +
    + +

    msg( 'tagline' ) ?>

    + + +
    html('userlangattributes') ?>>html( 'subtitle' ) ?>
    + + data['undelete'] ): ?> + +
    html( 'undelete' ) ?>
    + + + data['newtalk'] ): ?> + +
    html( 'newtalk' ) ?>
    + + + data['showjumplinks'] ): ?> + + + + + + html( 'bodytext' ) ?> + + data['catlinks'] ): ?> + + html( 'catlinks' ); ?> + + + data['dataAfterContent'] ): ?> + + html( 'dataAfterContent' ); ?> + + +
    +
    + +
    + + +
    + renderNavigation( 'PERSONAL' ); ?> +
    + renderNavigation( array( 'NAMESPACES', 'VARIANTS' ) ); ?> +
    +
    + renderNavigation( array( 'VIEWS', 'ACTIONS', 'SEARCH' ) ); ?> +
    +
    + + +
    + + + + renderPortals( $this->data['sidebar'] ); ?> +
    + + + + + + + + html( 'bottomscripts' ); /* JS call to runBodyOnloadHook */ ?> + html( 'reporttime' ) ?> + data['debug'] ): ?> + + + + + $content ) { + echo "\n\n"; + switch( $name ) { + case 'SEARCH': + break; + case 'TOOLBOX': +?> +
    + html('userlangattributes') ?>>msg( 'toolbox' ) ?> +
    + +
    +
    +data['language_urls'] ) { +?> +
    + html('userlangattributes') ?>>msg( 'otherlanguages' ) ?> +
    +
      + data['language_urls'] as $langlink ): ?> +
    • + +
    +
    +
    + +
    skin->tooltip( 'p-' . $name ) ?>> + html('userlangattributes') ?>> +
    + + + + + +
    +
    +\n"; + } + } + + /** + * Render one or more navigations elements by name, automatically reveresed + * when UI is in RTL mode + */ + private function renderNavigation( $elements ) { + global $wgContLang, $wgVectorUseSimpleSearch, $wgStylePath; + + // If only one element was given, wrap it in an array, allowing more + // flexible arguments + if ( !is_array( $elements ) ) { + $elements = array( $elements ); + // If there's a series of elements, reverse them when in RTL mode + } else if ( $wgContLang->isRTL() ) { + $elements = array_reverse( $elements ); + } + // Render elements + foreach ( $elements as $name => $element ) { + echo "\n\n"; + switch ( $element ) { + case 'NAMESPACES': +?> +
    +
    msg('namespaces') ?>
    + html('userlangattributes') ?>> + data['namespace_urls'] as $key => $link ): ?> +
  • >>
  • + + +
    + +
    +
    msg('variants') ?>
    + +
    + +
    +
    msg('views') ?>
    + html('userlangattributes') ?>> + data['view_urls'] as $key => $link ): ?> + >>' : ''.htmlspecialchars( $link['text'] ).'') ?> + + +
    + +
    +
    msg('actions') ?>
    + +
    + +
    +
    msg('personaltools') ?>
    + html('userlangattributes') ?>> + data['personal_urls'] as $key => $item): ?> +
  • > class="">
  • + + +
    + + +\n"; + } + } +} diff --git a/skins/archlinux/IEMacFixes.css b/skins/archlinux/IEMacFixes.css deleted file mode 100644 index f1b05719..00000000 --- a/skins/archlinux/IEMacFixes.css +++ /dev/null @@ -1,44 +0,0 @@ -/* IE/Mac only fix stylesheet, imported from main.css */ -#portal-column-content { - margin: 0 0 4.8em 0; - float: none; -} -#portal-column-content #content { - z-index: 0; -} -#portal-column-one { - position: absolute; - top: 0; - left: 0; - z-index: 3; -} -#portal-footer { - margin-left: 12em; -} -/* -#portlet-contentViews { - top: 0.6em !important; - left: 14.5em !important; -} -*/ -#portlet-contentViews li, -#portlet-contentViews .selected { - border: none !important; -} -#portlet-contentViews li a { - border: 1px solid #aaaaaa; - border-bottom: none; -} -#portlet-contentViews li.selected a { - border: 1px solid #fabd23; - border-bottom: none; -} -/* no background images */ -li#personaltools-userpage, -li#personaltools-login/* */ { - background: none; - padding-left: none; -} -#mactest { - color: green; -} diff --git a/skins/archlinux/discussionitem_icon.gif b/skins/archlinux/discussionitem_icon.gif index baec471a..e3ca6d9e 100644 Binary files a/skins/archlinux/discussionitem_icon.gif and b/skins/archlinux/discussionitem_icon.gif differ diff --git a/skins/archlinux/file_icon.gif b/skins/archlinux/file_icon.gif index 847f6485..69dbeaf7 100644 Binary files a/skins/archlinux/file_icon.gif and b/skins/archlinux/file_icon.gif differ diff --git a/skins/archlinux/link_icon.gif b/skins/archlinux/link_icon.gif index 815ccb1b..168c1a2f 100644 Binary files a/skins/archlinux/link_icon.gif and b/skins/archlinux/link_icon.gif differ diff --git a/skins/archlinux/lock_icon.gif b/skins/archlinux/lock_icon.gif index 8a87e283..f71cd9b8 100644 Binary files a/skins/archlinux/lock_icon.gif and b/skins/archlinux/lock_icon.gif differ diff --git a/skins/archlinux/magnify-clip.png b/skins/archlinux/magnify-clip.png index 992aa2e3..ffd7637f 100644 Binary files a/skins/archlinux/magnify-clip.png and b/skins/archlinux/magnify-clip.png differ diff --git a/skins/archlinux/mail_icon.gif b/skins/archlinux/mail_icon.gif index 50a87a9a..cf5680d9 100644 Binary files a/skins/archlinux/mail_icon.gif and b/skins/archlinux/mail_icon.gif differ diff --git a/skins/archlinux/main.css b/skins/archlinux/main.css index d6a67c1e..727355bb 100644 --- a/skins/archlinux/main.css +++ b/skins/archlinux/main.css @@ -54,9 +54,6 @@ body { margin: 0; padding: 0; } -.visualClear { - clear: both; -} /* general styles */ @@ -281,43 +278,12 @@ span.subpages { #siteNotice { text-align: center; font-size: 95%; - padding: 0 .9em; + padding: 0 0.9em; } #siteNotice p { margin: 0; padding: 0; } -.success { - color: green; - font-size: larger; -} -.error { - color: red; - font-size: larger; -} -.errorbox, .successbox { - font-size: larger; - border: 2px solid; - padding: .5em 1em; - float: left; - margin-bottom: 2em; - color: #000; -} -.errorbox { - border-color: red; - background-color: #fff2f2; -} -.successbox { - border-color: green; - background-color: #dfd; -} -.errorbox h2, .successbox h2 { - font-size: 1em; - font-weight: bold; - display: inline; - margin: 0 .5em 0 0; - border: none; -} .catlinks { border: 1px solid #aaa; @@ -525,35 +491,34 @@ table.rimage { ** this is css3, the validator doesn't like it when validating as css2 */ #bodyContent a.external, -#bodyContent a[href ^="gopher://"] { +#bodyContent a.external[href ^="gopher://"] { background: url(external.png) center right no-repeat; padding: 0 13px; } .rtl #bodyContent a.external, -.rtl #bodyContent a[href ^="gopher://"] { +.rtl #bodyContent a.external[href ^="gopher://"] { background-image: url(external-rtl.png); } -#bodyContent a[href ^="https://"], +#bodyContent a.external[href ^="https://"], .link-https { background: url(lock_icon.gif) center right no-repeat; padding: 0 16px; } -#bodyContent a[href ^="mailto:"], +#bodyContent a.external[href ^="mailto:"], .link-mailto { background: url(mail_icon.gif) center right no-repeat; padding: 0 18px; } -#bodyContent a[href ^="news://"] { +#bodyContent a.external[href ^="news://"] { background: url(news_icon.png) center right no-repeat; padding: 0 18px; } -#bodyContent a[href ^="ftp://"], +#bodyContent a.external[href ^="ftp://"], .link-ftp { background: url(file_icon.gif) center right no-repeat; padding: 0 18px; } -#bodyContent a[href ^="irc://"], -#bodyContent a.extiw[href ^="irc://"], +#bodyContent a.external[href ^="irc://"], .link-irc { background: url(discussionitem_icon.gif) center right no-repeat; padding: 0 18px; @@ -604,18 +569,10 @@ table.rimage { #bodyContent a.extiw, #bodyContent a.extiw:active { color: #36b; - background: none; - padding: 0; } #bodyContent a.external { color: #36b; } -/* this can be used in the content area to switch off -special external link styling */ -#bodyContent .plainlinks a { - background: none !important; - padding: 0 !important; -} /* ** Structural Elements */ @@ -920,6 +877,14 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { z-index: 3; } +/* Override text-transform on languages where capitalization is significant */ +.capitalize-all-nouns .portlet h5, +.capitalize-all-nouns .portlet h6, +.capitalize-all-nouns #p-personal ul, +.capitalize-all-nouns #p-cactions ul li a { + text-transform: none; +} + /* TODO: #t-iscite is only used by the Cite extension, come up with some * system which allows extensions to add to this file on the fly */ @@ -956,6 +921,13 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { height: 1%; } +.mw-htmlform-submit { + font-weight: bold; + padding-left: .3em; + padding-right: .3em; + margin-right: 2em; +} + /* js pref toc */ #preftoc { margin: 0; @@ -1005,10 +977,6 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { cursor: default; text-decoration: none; } -#prefcontrol { - padding-top: 2em; - clear: both; -} #preferences { margin: 0; border: 1px solid #aaa; @@ -1021,11 +989,7 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { padding: 0; margin: 0; } -.prefsection fieldset { - border: 1px solid #aaa; - float: left; - margin-right: 2em; -} + .prefsection legend { font-weight: bold; } @@ -1035,16 +999,11 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { .mainLegend { display: none; } -div.prefsectiontip { +td.htmlform-tip { font-size: x-small; padding: .2em 2em; color: #666; } -.btnSavePrefs { - font-weight: bold; - padding-left: .3em; - padding-right: .3em; -} .preferences-login { clear: both; @@ -1162,20 +1121,9 @@ div#userloginForm .captcha { display: none; } -.not-patrolled { - background-color: #ffa; -} + div.patrollink { clear: both; - font-size: 75%; - text-align: right; -} -span.newpage, span.minor, span.bot { - font-weight: bold; -} -span.unpatrolled { - font-weight: bold; - color: red; } .sharedUploadNotice { @@ -1187,66 +1135,14 @@ span.updatedmarker { background-color: #0f0; } -table.gallery { - border: 1px solid #ccc; - margin: 2px; - padding: 2px; - background-color: white; -} - -table.gallery tr { - vertical-align: top; -} - -table.gallery td { - vertical-align: top; - background-color: #f9f9f9; - border: solid 2px white; -} -/* Keep this temporarily so that cached pages will display right */ -table.gallery td.galleryheader { - text-align: center; - font-weight: bold; -} -table.gallery caption { - font-weight: bold; -} - -div.gallerybox { - margin: 2px; -} - -div.gallerybox div.thumb { - text-align: center; - border: 1px solid #ccc; - margin: 2px; -} - -div.gallerytext { - overflow: hidden; - font-size: 94%; - padding: 2px 4px; -} - -span.comment { - font-style: italic; -} - -span.changedby { - font-size: 95%; -} - .previewnote { - text-indent: 3em; color: #c00; - border-bottom: 1px solid #aaa; - padding-bottom: 1em; margin-bottom: 1em; } .previewnote p { - margin: 0; - padding: 0; + text-indent: 3em; + margin: 0.8em 0; } .editExternally { @@ -1268,69 +1164,6 @@ span.changedby { text-indent: -2em; } -/* Classes for EXIF data display */ -table.mw_metadata { - font-size: 0.8em; - margin-left: 0.5em; - margin-bottom: 0.5em; - width: 300px; -} - -table.mw_metadata caption { - font-weight: bold; -} - -table.mw_metadata th { - font-weight: normal; -} - -table.mw_metadata td { - padding: 0.1em; -} - -table.mw_metadata { - border: none; - border-collapse: collapse; -} - -table.mw_metadata td, table.mw_metadata th { - text-align: center; - border: 1px solid #aaaaaa; - padding-left: 0.1em; - padding-right: 0.1em; -} - -table.mw_metadata th { - background-color: #f9f9f9; -} - -table.mw_metadata td { - background-color: #fcfcfc; -} - -table.collapsed tr.collapsable { - display: none; -} - - -/* filetoc */ -ul#filetoc { - text-align: center; - border: 1px solid #aaaaaa; - background-color: #f9f9f9; - padding: 5px; - font-size: 95%; - margin-bottom: 0.5em; - margin-left: 0; - margin-right: 0; -} - -#filetoc li { - display: inline; - list-style-type: none; - padding-right: 2em; -} - input#wpSummary { width: 80%; } @@ -1368,32 +1201,6 @@ p.revision_saved { font-weight:bold; } -#mw_trackbacks { - border: solid 1px #bbbbff; - background-color: #eeeeff; - padding: 0.2em; -} - - -/* Allmessages table */ - -#allmessagestable th { - background-color: #b2b2ff; -} - -#allmessagestable tr.orig { - background-color: #ffe2e2; -} - -#allmessagestable tr.new { - background-color: #e2ffe2; -} - -#allmessagestable tr.def { - background-color: #f0f0ff; -} - - /* noarticletext */ div.noarticletext { border: 1px solid #ccc; @@ -1457,53 +1264,6 @@ table.multipageimage td { text-align: center; } -/** Special:Version */ - -table#sv-ext, table#sv-hooks, table#sv-software { - margin: 1em; - padding:0em; -} - -#sv-ext td, #sv-hooks td, #sv-software td, -#sv-ext th, #sv-hooks th, #sv-software th { - border: 1px solid #A0A0A0; - padding: 0 0.15em 0 0.15em; -} -#sv-ext th, #sv-hooks th, #sv-software th { - background-color: #F0F0F0; - color: black; - padding: 0 0.15em 0 0.15em; -} -tr.sv-space{ - height: 0.8em; - border:none; -} -tr.sv-space td { display: none; } - -/* - Table pager (e.g. Special:Imagelist) - - remove underlines from the navigation link - - collapse borders - - set the borders to outsets (similar to Special:Allmessages) - - remove line wrapping for all td and th, set background color - - restore line wrapping for the last two table cells (description and size) -*/ -.TablePager { min-width: 80%; } -.TablePager_nav a { text-decoration: none; } -.TablePager { border-collapse: collapse; } -.TablePager, .TablePager td, .TablePager th { - border: 1px solid #aaaaaa; - padding: 0 0.15em 0 0.15em; -} -.TablePager th { background-color: #eeeeff } -.TablePager td { background-color: #ffffff } -.TablePager tr:hover td { background-color: #eeeeff } - -.imagelist td, .imagelist th { white-space: nowrap } -.imagelist .TablePager_col_links { background-color: #eeeeff } -.imagelist .TablePager_col_img_description { white-space: normal } -.imagelist th.TablePager_sort { background-color: #ccccff } - .templatesUsed { margin-top: 1.5em; } .mw-summary-preview { @@ -1537,24 +1297,12 @@ div.mw-lag-warn-high { font-size: 90%; } -/** Special:Search stuff */ -div#mw-search-interwiki-caption { - text-align: center; - font-weight: bold; - font-size: 95%; -} - -.mw-search-interwiki-project { - font-size: 97%; - text-align: left; - padding-left: 0.2em; - padding-right: 0.15em; - padding-bottom: 0.2em; - padding-top: 0.15em; - background: #cae8ff; -} - /* God-damned hack for the crappy layout */ .os-suggest { font-size: 127%; } + +/* Sometimes people don't want personal tools to be lowercase! */ +.no-text-transform { + text-transform: none; +} diff --git a/skins/archlinux/rtl.css b/skins/archlinux/rtl.css index 9b8e4f44..b9bf43c0 100644 --- a/skins/archlinux/rtl.css +++ b/skins/archlinux/rtl.css @@ -211,7 +211,6 @@ input#wpSave, input#wpDiff { } #userlogin { - float: right; margin: 0 0 1em 3em; } /* Convenience links to edit block, delete and protect reasons */ @@ -228,20 +227,25 @@ table.filehistory th { text-align: right; } +/* Special:Allpages styling */ +td.mw-allpages-nav, p.mw-allpages-nav, td.mw-allpages-alphaindexline { + text-align: left; +} + +/* Special:Prefixindex styling */ +td#mw-prefixindex-nav-form { + text-align: left; +} + /** * Lists: * The following lines don't have a visible effect on non-Gecko browsers * They fix a problem ith Gecko browsers rendering lists to the right of * left-floated objects in an RTL layout. */ -html > body div#bodyContent ul { +html > body div#article ul { display: table; } html > body div#bodyContent ul#filetoc { display: block; } -/* Special:Prefixindex styling */ -td#mw-prefixindex-nav-form { - text-align: left; -} - diff --git a/skins/archlinux/user.gif b/skins/archlinux/user.gif index c9c9ab96..34b4839d 100644 Binary files a/skins/archlinux/user.gif and b/skins/archlinux/user.gif differ diff --git a/skins/archlinux/video.png b/skins/archlinux/video.png index 38103dac..fadc4c9b 100644 Binary files a/skins/archlinux/video.png and b/skins/archlinux/video.png differ diff --git a/skins/archlinux/wiki.png b/skins/archlinux/wiki.png index 69fce988..44389389 100644 Binary files a/skins/archlinux/wiki.png and b/skins/archlinux/wiki.png differ diff --git a/skins/chick/main.css b/skins/chick/main.css index fde03301..ac61b91b 100644 --- a/skins/chick/main.css +++ b/skins/chick/main.css @@ -7,10 +7,11 @@ */ body { - font-family: sans-serif; - color: Black; - margin: 0; - padding: 0.3em; + font-family: sans-serif; + color: black; + background: white; + margin: 0; + padding: 0.3em; } a { color: #002bb8; } @@ -23,32 +24,32 @@ a.new:visited, #p-personal a.new:visited { color:#a55858; } img { - border: none; - vertical-align: middle; + border: none; + vertical-align: middle; } p { - margin: 0.4em 0em 0.5em 0em; - line-height: 1.5em; + margin: 0.4em 0em 0.5em 0em; + line-height: 1.5em; } p img { margin: 0; } hr { - height: 1px; - color: #aaaaaa; - background-color: #aaaaaa; - border: 0; - margin: 0.2em 0 0.2em 0; + height: 1px; + color: #aaaaaa; + background-color: #aaaaaa; + border: 0; + margin: 0.2em 0 0.2em 0; } h1, h2, h3, h4, h5, h6 { - color: Black; - background: none; - font-weight: normal; - margin: 0; - padding-top: 0.5em; - padding-bottom: 0.17em; - border-bottom: 1px solid #aaaaaa; + color: black; + background: none; + font-weight: normal; + margin: 0; + padding-top: 0.5em; + padding-bottom: 0.17em; + border-bottom: 1px solid #aaaaaa; } .editsection { font-weight: normal; @@ -58,8 +59,8 @@ h1 .editsection { font-size: 53.2%; } h2 { font-size: 150%; } h2 .editsection { font-size: 66.7%; } h3, h4, h5, h6 { - border-bottom: none; - font-weight: bold; + border-bottom: none; + font-weight: bold; } h3 { font-size: 132%; } h3 .editsection { font-size: 75.8%; } @@ -70,97 +71,97 @@ h6 { font-size: 80%; } h6 .editsection { font-size: 125%; } ul { - line-height: 1.5em; - margin: 0.3em 0 0 1.5em; - padding:0; + line-height: 1.5em; + margin: 0.3em 0 0 1.5em; + padding:0; } ol { - line-height: 1.5em; - margin: 0.3em 0 0 3.2em; - padding:0; - list-style-image: none; + line-height: 1.5em; + margin: 0.3em 0 0 3.2em; + padding:0; + list-style-image: none; } li { margin-bottom: 0.1em; } dt { - font-weight: bold; - margin-bottom: 0.1em; + font-weight: bold; + margin-bottom: 0.1em; } dl{ - margin-top: 0.2em; - margin-bottom: 0.5em; + margin-top: 0.2em; + margin-bottom: 0.5em; } dd { - line-height: 1.5em; - margin-left: 2em; - margin-bottom: 0.1em; + line-height: 1.5em; + margin-left: 2em; + margin-bottom: 0.1em; } fieldset { - border: 1px solid #2f6fab; - margin: 1em 0em 1em 0em; - padding: 0em 1em 1em 1em; - line-height: 1.5em; + border: 1px solid #2f6fab; + margin: 1em 0em 1em 0em; + padding: 0em 1em 1em 1em; + line-height: 1.5em; } legend { - background: White; - padding: 0.5em; - font-size: 95%; + background: white; + padding: 0.5em; + font-size: 95%; } form { - border: none; - margin: 0; + border: none; + margin: 0; } textarea { - border: 1px solid #2f6fab; - color: Black; - background-color: white; - width: 100%; - padding: 0.1em; - overflow: auto; + border: 1px solid #2f6fab; + color: black; + background-color: white; + width: 100%; + padding: 0.1em; + overflow: auto; } /* hide this from ie/mac and konq2.2 */ @media All { - head:first-child+body input { - visibility: visible; - border: 1px solid #2f6fab; - color: Black; - background-color: white; - vertical-align: middle; - padding: 0.2em; - } + head:first-child+body input { + visibility: visible; + border: 1px solid #2f6fab; + color: black; + background-color: white; + vertical-align: middle; + padding: 0.2em; + } } input.historysubmit { - padding: 0 0.3em 0.3em 0.3em !important; - font-size: 94%; - cursor: pointer; - height: 1.7em !important; - margin-left: 1.6em; + padding: 0 0.3em 0.3em 0.3em !important; + font-size: 94%; + cursor: pointer; + height: 1.7em !important; + margin-left: 1.6em; } input[type="radio"], input[type="checkbox"] { border:none; } select { - border: 1px solid #2f6fab; - color: Black; - vertical-align: top; + border: 1px solid #2f6fab; + color: black; + vertical-align: top; } abbr, acronym, .explain { - border-bottom: 1px dotted Black; - color: Black; - background: none; - cursor: help; + border-bottom: 1px dotted black; + color: black; + background: none; + cursor: help; } q { - font-family: Times, "Times New Roman", serif; - font-style: italic; + font-family: Times, "Times New Roman", serif; + font-style: italic; } code { background-color: #f9f9f9; } pre { - padding: 1em; - border: 1px dashed #2f6fab; - color: Black; - background-color: #f9f9f9; - line-height: 1.1em; + padding: 1em; + border: 1px dashed #2f6fab; + color: black; + background-color: #f9f9f9; + line-height: 1.1em; } /* @@ -174,36 +175,36 @@ span.subpages { display: block; } #bodyContent h3, #bodyContent h4, #bodyContent h5 { - margin-bottom: 0.3em; + margin-bottom: 0.3em; } #firstHeading { margin-bottom:0.1em; } /* user notification thing */ .usermessage { - background-color: #ffce7b; - border: 1px solid #ffa500; - color: Black; - font-weight: bold; - margin: 0.1em 0 0 0; - padding: 2px 5px; - vertical-align: middle; + background-color: #ffce7b; + border: 1px solid #ffa500; + color: black; + font-weight: bold; + margin: 0.1em 0 0 0; + padding: 2px 5px; + vertical-align: middle; } #siteNotice { - text-align: center; - font-size: 95%; - padding: 0 0.9em 0 0.9em; + text-align: center; + font-size: 95%; + padding: 0 0.9em 0 0.9em; } #siteNotice p { margin: 0; padding: 0; } .error { - color: red; - font-size: larger; + color: red; + font-size: larger; } .catlinks { - border:1px solid #aaaaaa; - background-color:#f9f9f9; - padding: 2px 5px; - margin: 0.1em 0 0 0; - clear: both; + border:1px solid #aaaaaa; + background-color:#f9f9f9; + padding: 2px 5px; + margin: 0.1em 0 0 0; + clear: both; } .catlinks { margin: 0; padding: 0; } @@ -211,28 +212,28 @@ span.subpages { display: block; } /* currently unused, intended to be used by a metadata box in the bottom-right corner of the content area */ .documentDescription { - /* The summary text describing the document */ - font-weight: bold; - display: block; - margin: 1em 0em; - line-height: 1.5em; + /* The summary text describing the document */ + font-weight: bold; + display: block; + margin: 1em 0em; + line-height: 1.5em; } .documentByLine { - text-align: right; - font-size: 90%; - clear: both; - font-weight: normal; - color: #76797c; + text-align: right; + font-size: 90%; + clear: both; + font-weight: normal; + color: #76797c; } /* emulate center */ .center { - width: 100%; - text-align: center; + width: 100%; + text-align: center; } *.center * { - margin-left: auto; - margin-right: auto; + margin-left: auto; + margin-right: auto; } /* small for tables and similar */ .small, .small * { font-size: 94%; } @@ -243,88 +244,88 @@ table.small { font-size: 100% } */ #toc { - /*border:1px solid #2f6fab;*/ - border:1px solid #aaaaaa; - background-color:#f9f9f9; - padding:5px; - font-size: 95%; + /*border:1px solid #2f6fab;*/ + border:1px solid #aaaaaa; + background-color:#f9f9f9; + padding:5px; + font-size: 95%; } #toc ul { margin-left: 2em; } #toc .toctoggle { font-size: 94%; } #toc .editsection { - margin-top: 0.7em; - font-size: 94%; + margin-top: 0.7em; + font-size: 94%; } /* images */ div.floatright, table.floatright { - clear: right; - float: right; - margin: 0; - position: relative; - border: 0.5em solid White; - border-width: 0.5em 0 0.8em 1.4em; + clear: right; + float: right; + margin: 0; + position: relative; + border: 0.5em solid white; + border-width: 0.5em 0 0.8em 1.4em; } div.floatright p { font-style: italic; } div.floatleft, table.floatleft { - float: left; - clear: left; - margin: 0.3em 0.5em 0.5em 0; - position: relative; - border: 0.5em solid White; - border-width: 0.5em 1.4em 0.8em 0; + float: left; + clear: left; + margin: 0.3em 0.5em 0.5em 0; + position: relative; + border: 0.5em solid white; + border-width: 0.5em 1.4em 0.8em 0; } div.floatleft p { font-style: italic; } /* thumbnails */ div.thumb { - margin-bottom: 0.5em; - border-style: solid; border-color: White; - width: auto; -} -div.thumb div { - border:1px solid #cccccc; - padding: 3px !important; - background-color:#f9f9f9; - font-size: 94%; - text-align: center; - overflow: hidden; -} -div.thumb div a img { - border:1px solid #cccccc; -} -div.thumb div div.thumbcaption { - border: none; - text-align: left; - line-height: 1.4em; - padding: 0.3em 0 0.1em 0; + margin-bottom: 0.5em; + border-style: solid; border-color: white; + width: auto; +} +div.thumbinner { + border:1px solid #cccccc; + padding: 3px !important; + background-color:#f9f9f9; + font-size: 94%; + text-align: center; + overflow: hidden; +} +html .thumbimage { + border:1px solid #cccccc; +} +html .thumbcaption { + border: none; + text-align: left; + line-height: 1.4em; + padding: 0.3em 0 0.1em 0; } div.magnify { - float: right; - border: none !important; - background: none !important; + float: right; + border: none !important; + background: none !important; } div.magnify a, div.magnify img { - display: block; - border: none !important; - background: none !important; + display: block; + border: none !important; + background: none !important; } div.tright { - clear: right; - float: right; - border-width: 0.5em 0 0.8em 1.4em; + clear: right; + float: right; + border-width: 0.5em 0 0.8em 1.4em; } div.tleft { - float: left; - clear: left; - margin-right:0.5em; - border-width: 0.5em 1.4em 0.8em 0; + float: left; + clear: left; + margin-right:0.5em; + border-width: 0.5em 1.4em 0.8em 0; } img.thumbborder { border: 1px solid #dddddd; } .hiddenStructure { - display: none; + display: none; } /* @@ -337,26 +338,26 @@ img.thumbborder { */ /* table standards */ table.rimage { - float:right; - position:relative; - margin-left:1em; - margin-bottom:1em; - text-align:center; + float:right; + position:relative; + margin-left:1em; + margin-bottom:1em; + text-align:center; } .toccolours { - border:1px solid #aaaaaa; - background-color:#f9f9f9; - padding:5px; - font-size: 95%; + border:1px solid #aaaaaa; + background-color:#f9f9f9; + padding:5px; + font-size: 95%; } /* ** edit views etc */ .special li { - line-height: 1.4em; - margin: 0; - padding: 0; + line-height: 1.4em; + margin: 0; + padding: 0; } a.external { color: #3366bb; } @@ -376,51 +377,18 @@ div.patrollink { font-size: 75%; text-align: right; } -span.newpage, span.minor { - font-weight: bold; -} span.updatedmarker { color:black; background-color:#00FF00; } -table.gallery { - border: 1px solid #cccccc; - margin: 2px; - padding: 2px; - background-color:#ffffff; -} - -table.gallery tr { - vertical-align:top; -} - -table.gallery td { - vertical-align:top; - background-color:#f9f9f9; - border: solid 2px white; -} - div.gallerybox { - margin: 2px; - width: 150px; + width: 150px; } -div.gallerybox div.thumb { - text-align: center; - border: 1px solid #cccccc; - margin: 2px; -} - -div.gallerytext { - overflow: hidden; - font-size: 94%; - padding: 2px 4px; -} - #xjump-to-nav { - display: none; + display: none; } .templatesUsed { margin-top: 1.5em; } @@ -440,4 +408,4 @@ div.gallerytext { } #f-poweredbyico, #f-copyrightico { display: inline; -} \ No newline at end of file +} diff --git a/skins/common/IE80Fixes.css b/skins/common/IE80Fixes.css new file mode 100644 index 00000000..b6360f60 --- /dev/null +++ b/skins/common/IE80Fixes.css @@ -0,0 +1,15 @@ +/** + * Fixes textarea scrolling bug (bug #19334). The bug only occurs when a + * percentage width is given, so instead of width: 100%, use min-width: 100%; + * max-width: 100%. We also need to give a fixed width for the actual width + * property for the hack to work, although the actual value (500px here) ends + * up being ignored; min/max-width take precedence. + * + * More info: http://grantovich.net/posts/2009/06/that-weird-ie8-textarea-bug/ + */ +#wpTextbox1 { + height: 390px; + width: 500px; + min-width: 100%; + max-width: 100%; +} diff --git a/skins/common/IEFixes.js b/skins/common/IEFixes.js index f85f506c..762d7a78 100644 --- a/skins/common/IEFixes.js +++ b/skins/common/IEFixes.js @@ -1,127 +1,128 @@ // IE fixes javascript -var isMSIE55 = (window.showModalDialog && window.clipboardData && window.createPopup); +var isMSIE55 = ( window.showModalDialog && window.clipboardData && window.createPopup ); var doneIETransform; var doneIEAlphaFix; -if (document.attachEvent) - document.attachEvent('onreadystatechange', hookit); +if ( document.attachEvent ) { + document.attachEvent( 'onreadystatechange', hookit ); +} function hookit() { - if (!doneIETransform && document.getElementById && document.getElementById('bodyContent')) { - doneIETransform = true; - relativeforfloats(); - fixalpha(); - } + if ( !doneIETransform && document.getElementById && document.getElementById( 'bodyContent' ) ) { + doneIETransform = true; + relativeforfloats(); + fixalpha(); + } } // png alpha transparency fixes -function fixalpha() { - // bg - if (isMSIE55 && !doneIEAlphaFix) - { - var plogo = document.getElementById('p-logo'); - if (!plogo) return; - - var logoa = plogo.getElementsByTagName('a')[0]; - if (!logoa) return; - - var bg = logoa.currentStyle.backgroundImage; - var imageUrl = bg.substring(5, bg.length-2); - - doneIEAlphaFix = true; - - if (imageUrl.substr(imageUrl.length-4).toLowerCase() == '.png') { - var logospan = logoa.appendChild(document.createElement('span')); - - logoa.style.backgroundImage = 'none'; - logospan.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=' + imageUrl + ')'; - logospan.style.height = '100%'; - logospan.style.position = 'absolute'; - logospan.style.width = logoa.currentStyle.width; - logospan.style.cursor = 'hand'; - // Center image with hack for IE5.5 - if (document.documentElement.dir == "rtl") - { - logospan.style.right = '50%'; - logospan.style.setExpression('marginRight', '"-" + (this.offsetWidth / 2) + "px"'); - } - else - { - logospan.style.left = '50%'; - logospan.style.setExpression('marginLeft', '"-" + (this.offsetWidth / 2) + "px"'); - } - logospan.style.top = '50%'; - logospan.style.setExpression('marginTop', '"-" + (this.offsetHeight / 2) + "px"'); - - var linkFix = logoa.appendChild(logoa.cloneNode()); - linkFix.style.position = 'absolute'; - linkFix.style.height = '100%'; - linkFix.style.width = '100%'; - } - } +function fixalpha( logoId ) { + // bg + if ( isMSIE55 && !doneIEAlphaFix ) { + var plogo = document.getElementById( logoId || 'p-logo' ); + if ( !plogo ) { + return; + } + + var logoa = plogo.getElementsByTagName('a')[0]; + if ( !logoa ) { + return; + } + + var bg = logoa.currentStyle.backgroundImage; + var imageUrl = bg.substring( 5, bg.length - 2 ); + + doneIEAlphaFix = true; + + if ( imageUrl.substr( imageUrl.length - 4 ).toLowerCase() == '.png' ) { + var logospan = logoa.appendChild( document.createElement( 'span' ) ); + + logoa.style.backgroundImage = 'none'; + logospan.style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=' + imageUrl + ')'; + logospan.style.height = '100%'; + logospan.style.position = 'absolute'; + logospan.style.width = logoa.currentStyle.width; + logospan.style.cursor = 'hand'; + // Center image with hack for IE5.5 + if ( document.documentElement.dir == 'rtl' ) { + logospan.style.right = '50%'; + logospan.style.setExpression( 'marginRight', '"-" + (this.offsetWidth / 2) + "px"' ); + } else { + logospan.style.left = '50%'; + logospan.style.setExpression( 'marginLeft', '"-" + (this.offsetWidth / 2) + "px"' ); + } + logospan.style.top = '50%'; + logospan.style.setExpression( 'marginTop', '"-" + (this.offsetHeight / 2) + "px"' ); + + var linkFix = logoa.appendChild( logoa.cloneNode() ); + linkFix.style.position = 'absolute'; + linkFix.style.height = '100%'; + linkFix.style.width = '100%'; + } + } } // fix ie6 disappering float bug function relativeforfloats() { - var bc = document.getElementById('bodyContent'); - if (bc) { - var tables = bc.getElementsByTagName('table'); - var divs = bc.getElementsByTagName('div'); - } - setrelative(tables); - setrelative(divs); + var bc = document.getElementById( 'bodyContent' ); + if ( bc ) { + var tables = bc.getElementsByTagName( 'table' ); + var divs = bc.getElementsByTagName( 'div' ); + } + setrelative( tables ); + setrelative( divs ); } -function setrelative (nodes) { - var i = 0; - while (i < nodes.length) { - if(((nodes[i].style.float && nodes[i].style.float != ('none') || - (nodes[i].align && nodes[i].align != ('none'))) && - (!nodes[i].style.position || nodes[i].style.position != 'relative'))) - { - nodes[i].style.position = 'relative'; - } - i++; - } +function setrelative( nodes ) { + var i = 0; + while ( i < nodes.length ) { + if( ( ( nodes[i].style.float && nodes[i].style.float != ( 'none' ) || + ( nodes[i].align && nodes[i].align != ( 'none' ) ) ) && + ( !nodes[i].style.position || nodes[i].style.position != 'relative' ) ) ) + { + nodes[i].style.position = 'relative'; + } + i++; + } } - // Expand links for printing - -String.prototype.hasClass = function(classWanted) -{ - var classArr = this.split(/\s/); - for (var i=0; i body div#article ul { display: table; } +html > body div#bodyContent ul#filetoc { + display: block; +} /* feed links */ a.feedlink { diff --git a/skins/common/edit.js b/skins/common/edit.js index 945059e0..423205f8 100644 --- a/skins/common/edit.js +++ b/skins/common/edit.js @@ -1,137 +1,163 @@ +var currentFocused; + // this function generates the actual toolbar buttons with localized text // we use it to avoid creating the toolbar where javascript is not enabled -function addButton(imageFile, speedTip, tagOpen, tagClose, sampleText, imageId) { +function addButton( imageFile, speedTip, tagOpen, tagClose, sampleText, imageId ) { // Don't generate buttons for browsers which don't fully // support it. - mwEditButtons[mwEditButtons.length] = - {"imageId": imageId, - "imageFile": imageFile, - "speedTip": speedTip, - "tagOpen": tagOpen, - "tagClose": tagClose, - "sampleText": sampleText}; + mwEditButtons.push({ + 'imageId': imageId, + 'imageFile': imageFile, + 'speedTip': speedTip, + 'tagOpen': tagOpen, + 'tagClose': tagClose, + 'sampleText': sampleText + }); } // this function generates the actual toolbar buttons with localized text -// we use it to avoid creating the toolbar where javascript is not enabled -function mwInsertEditButton(parent, item) { - var image = document.createElement("img"); +// we use it to avoid creating the toolbar where JavaScript is not enabled +function mwInsertEditButton( parent, item ) { + var image = document.createElement( 'img' ); image.width = 23; image.height = 22; - image.className = "mw-toolbar-editbutton"; - if (item.imageId) image.id = item.imageId; + image.className = 'mw-toolbar-editbutton'; + if ( item.imageId ) { + image.id = item.imageId; + } image.src = item.imageFile; image.border = 0; image.alt = item.speedTip; image.title = item.speedTip; - image.style.cursor = "pointer"; + image.style.cursor = 'pointer'; image.onclick = function() { - insertTags(item.tagOpen, item.tagClose, item.sampleText); + insertTags( item.tagOpen, item.tagClose, item.sampleText ); + // click tracking + if ( ( typeof $j != 'undefined' ) && ( typeof $j.trackAction != 'undefined' ) ) { + $j.trackAction( 'oldedit.' + item.speedTip.replace(/ /g, "-") ); + } return false; }; - parent.appendChild(image); + parent.appendChild( image ); return true; } function mwSetupToolbar() { - var toolbar = document.getElementById('toolbar'); - if (!toolbar) { return false; } - - var textbox = document.getElementById('wpTextbox1'); - if (!textbox) { return false; } + var toolbar = document.getElementById( 'toolbar' ); + if ( !toolbar ) { + return false; + } // Don't generate buttons for browsers which don't fully // support it. - if (!(document.selection && document.selection.createRange) - && textbox.selectionStart === null) { + // but don't assume wpTextbox1 is always here + var textboxes = document.getElementsByTagName( 'textarea' ); + if ( !textboxes.length ) { + // No toolbar if we can't find any textarea return false; } - - for (var i = 0; i < mwEditButtons.length; i++) { - mwInsertEditButton(toolbar, mwEditButtons[i]); + // Only check for selection capability if the textarea is visible - errors will occur otherwise - just because + // the textarea is not visible, doesn't mean we shouldn't build out the toolbar though - it might have been replaced + // with some other kind of control + if ( textboxes[0].style.display != 'none' ) { + if ( !( document.selection && document.selection.createRange ) + && textboxes[0].selectionStart === null ) { + return false; + } + } + for ( var i = 0; i < mwEditButtons.length; i++ ) { + mwInsertEditButton( toolbar, mwEditButtons[i] ); } - for (var i = 0; i < mwCustomEditButtons.length; i++) { - mwInsertEditButton(toolbar, mwCustomEditButtons[i]); + for ( var i = 0; i < mwCustomEditButtons.length; i++ ) { + mwInsertEditButton( toolbar, mwCustomEditButtons[i] ); } return true; } // apply tagOpen/tagClose to selection in textarea, // use sampleText instead of selection if there is none -function insertTags(tagOpen, tagClose, sampleText) { +function insertTags( tagOpen, tagClose, sampleText ) { + if ( typeof $j != 'undefined' && typeof $j.fn.textSelection != 'undefined' && + ( currentFocused.nodeName.toLowerCase() == 'iframe' || currentFocused.id == 'wpTextbox1' ) ) { + $j( '#wpTextbox1' ).textSelection( + 'encapsulateSelection', { 'pre': tagOpen, 'peri': sampleText, 'post': tagClose } + ); + return; + } var txtarea; - if (document.editform) { - txtarea = document.editform.wpTextbox1; + if ( document.editform ) { + txtarea = currentFocused; } else { // some alternate form? take the first one we can find - var areas = document.getElementsByTagName('textarea'); + var areas = document.getElementsByTagName( 'textarea' ); txtarea = areas[0]; } var selText, isSample = false; - if (document.selection && document.selection.createRange) { // IE/Opera - - //save window scroll position - if (document.documentElement && document.documentElement.scrollTop) + if ( document.selection && document.selection.createRange ) { // IE/Opera + // save window scroll position + if ( document.documentElement && document.documentElement.scrollTop ) { var winScroll = document.documentElement.scrollTop - else if (document.body) + } else if ( document.body ) { var winScroll = document.body.scrollTop; - //get current selection + } + // get current selection txtarea.focus(); var range = document.selection.createRange(); selText = range.text; - //insert tags + // insert tags checkSelectedText(); range.text = tagOpen + selText + tagClose; - //mark sample text as selected - if (isSample && range.moveStart) { - if (window.opera) + // mark sample text as selected + if ( isSample && range.moveStart ) { + if ( window.opera ) { tagClose = tagClose.replace(/\n/g,''); - range.moveStart('character', - tagClose.length - selText.length); - range.moveEnd('character', - tagClose.length); + } + range.moveStart('character', - tagClose.length - selText.length); + range.moveEnd('character', - tagClose.length); } - range.select(); - //restore window scroll position - if (document.documentElement && document.documentElement.scrollTop) - document.documentElement.scrollTop = winScroll - else if (document.body) + range.select(); + // restore window scroll position + if ( document.documentElement && document.documentElement.scrollTop ) { + document.documentElement.scrollTop = winScroll; + } else if ( document.body ) { document.body.scrollTop = winScroll; + } - } else if (txtarea.selectionStart || txtarea.selectionStart == '0') { // Mozilla - - //save textarea scroll position + } else if ( txtarea.selectionStart || txtarea.selectionStart == '0' ) { // Mozilla + // save textarea scroll position var textScroll = txtarea.scrollTop; - //get current selection + // get current selection txtarea.focus(); var startPos = txtarea.selectionStart; var endPos = txtarea.selectionEnd; - selText = txtarea.value.substring(startPos, endPos); - //insert tags + selText = txtarea.value.substring( startPos, endPos ); + // insert tags checkSelectedText(); txtarea.value = txtarea.value.substring(0, startPos) + tagOpen + selText + tagClose + txtarea.value.substring(endPos, txtarea.value.length); - //set new selection - if (isSample) { + // set new selection + if ( isSample ) { txtarea.selectionStart = startPos + tagOpen.length; txtarea.selectionEnd = startPos + tagOpen.length + selText.length; } else { txtarea.selectionStart = startPos + tagOpen.length + selText.length + tagClose.length; txtarea.selectionEnd = txtarea.selectionStart; } - //restore textarea scroll position + // restore textarea scroll position txtarea.scrollTop = textScroll; - } + } - function checkSelectedText(){ - if (!selText) { + function checkSelectedText() { + if ( !selText ) { selText = sampleText; isSample = true; - } else if (selText.charAt(selText.length - 1) == ' ') { //exclude ending space char + } else if ( selText.charAt(selText.length - 1) == ' ' ) { // exclude ending space char selText = selText.substring(0, selText.length - 1); - tagClose += ' ' - } + tagClose += ' '; + } } } @@ -144,13 +170,62 @@ function scrollEditBox() { var editBox = document.getElementById( 'wpTextbox1' ); var scrollTop = document.getElementById( 'wpScrolltop' ); var editForm = document.getElementById( 'editform' ); - if( editBox && scrollTop ) { - if( scrollTop.value ) + if( editForm && editBox && scrollTop ) { + if( scrollTop.value ) { editBox.scrollTop = scrollTop.value; + } addHandler( editForm, 'submit', function() { - document.getElementById( 'wpScrolltop' ).value = document.getElementById( 'wpTextbox1' ).scrollTop; + scrollTop.value = editBox.scrollTop; } ); } } hookEvent( 'load', scrollEditBox ); hookEvent( 'load', mwSetupToolbar ); +hookEvent( 'load', function() { + currentFocused = document.getElementById( 'wpTextbox1' ); + // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html + // focus does not bubble normally, but using a trick we can do event delegation + // on the focus event on all text inputs to make the toolbox usable on all of them + var editForm = document.getElementById( 'editform' ); + if ( !editForm ) { + return; + } + function onfocus( e ) { + var elm = e.target || e.srcElement; + if ( !elm ) { + return; + } + var tagName = elm.tagName.toLowerCase(); + var type = elm.type || ''; + if ( tagName !== 'textarea' && tagName !== 'input' ) { + return; + } + if ( tagName === 'input' && type.toLowerCase() !== 'text' ) { + return; + } + + currentFocused = elm; + } + + if ( editForm.addEventListener ) { + // Gecko, WebKit, Opera, etc... (all standards compliant browsers) + editForm.addEventListener( 'focus', onfocus, true ); // This MUST be true to work + } else if ( editForm.attachEvent ) { + // IE needs a specific trick here since it doesn't support the standard + editForm.attachEvent( 'onfocusin', function() { onfocus( event ); } ); + } + + // HACK: make currentFocused work with the usability iframe + // With proper focus detection support (HTML 5!) this'll be much cleaner + if ( typeof $j != 'undefined' ) { + var iframe = $j( '.wikiEditor-ui-text iframe' ); + if ( iframe.length > 0 ) { + $j( iframe.get( 0 ).contentWindow.document ) + .add( iframe.get( 0 ).contentWindow.document.body ) // for IE + .focus( function() { currentFocused = iframe.get( 0 ); } ); + } + } + + editForm +} ); + diff --git a/skins/common/history.js b/skins/common/history.js index 6a84b997..02651225 100644 --- a/skins/common/history.js +++ b/skins/common/history.js @@ -67,20 +67,19 @@ function diffcheck() { } else { inputs[1].style.visibility = 'visible'; } - lis[i].className = lis[i].classNameOriginal; + if ( typeof lis[i].classNameOriginal != 'undefined' ) { + lis[i].className = lis[i].classNameOriginal; + } } } } return true; } -// page history stuff -// attach event handlers to the input elements on history page +// Attach event handlers to the input elements on history page function histrowinit() { var hf = document.getElementById('pagehistory'); - if (!hf) { - return; - } + if (!hf) return; var lis = hf.getElementsByTagName('li'); for (var i = 0; i < lis.length; i++) { var inputs = historyRadios(lis[i]); diff --git a/skins/common/htmlform.js b/skins/common/htmlform.js new file mode 100644 index 00000000..2045ab48 --- /dev/null +++ b/skins/common/htmlform.js @@ -0,0 +1,40 @@ +// Find select-or-other fields. +addOnloadHook( function() { + var fields = getElementsByClassName( document, 'select', 'mw-htmlform-select-or-other' ); + + for( var i = 0; i < fields.length; i++ ) { + var select = fields[i]; + + addHandler( select, 'change', htmlforms.selectOrOtherSelectChanged ); + + // Use a fake 'e' to update it. + htmlforms.selectOrOtherSelectChanged( { 'target': select } ); + } +} ); + +var htmlforms = { + 'selectOrOtherSelectChanged' : function( e ) { + var select; + if ( !e ) { + e = window.event; + } + if ( e.target ) { + select = e.target; + } else if ( e.srcElement ) { + select = e.srcElement; + } + if ( select.nodeType == 3 ) { // defeat Safari bug + select = select.parentNode; + } + + var id = select.id; + var textbox = document.getElementById( id + '-other' ); + + if ( select.value == 'other' ) { + textbox.disabled = false; + } else { + textbox.disabled = true; + } + } +} + diff --git a/skins/common/images/Arr_.png b/skins/common/images/Arr_.png index 83fafc74..8d8d5d9c 100644 Binary files a/skins/common/images/Arr_.png and b/skins/common/images/Arr_.png differ diff --git a/skins/common/images/Arr_r.xcf b/skins/common/images/Arr_r.xcf deleted file mode 100644 index 83b7b2a8..00000000 Binary files a/skins/common/images/Arr_r.xcf and /dev/null differ diff --git a/skins/common/images/Arr_u.png b/skins/common/images/Arr_u.png index b8e3b6c6..75909865 100644 Binary files a/skins/common/images/Arr_u.png and b/skins/common/images/Arr_u.png differ diff --git a/skins/common/images/Zoom_sans.gif b/skins/common/images/Zoom_sans.gif index 6ba0adca..56a49de8 100644 Binary files a/skins/common/images/Zoom_sans.gif and b/skins/common/images/Zoom_sans.gif differ diff --git a/skins/common/images/add.png b/skins/common/images/add.png new file mode 100644 index 00000000..5b051f64 Binary files /dev/null and b/skins/common/images/add.png differ diff --git a/skins/common/images/ajax-loader.gif b/skins/common/images/ajax-loader.gif new file mode 100644 index 00000000..3288d103 Binary files /dev/null and b/skins/common/images/ajax-loader.gif differ diff --git a/skins/common/images/arrow_first.svg b/skins/common/images/arrow_first.svg deleted file mode 100644 index c1d8e364..00000000 --- a/skins/common/images/arrow_first.svg +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - image/svg+xml - - - - - - - - - diff --git a/skins/common/images/arrow_left.svg b/skins/common/images/arrow_left.svg deleted file mode 100644 index bd4bbc74..00000000 --- a/skins/common/images/arrow_left.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - image/svg+xml - - - - - - - - diff --git a/skins/common/images/be-tarask/button_bold.png b/skins/common/images/be-tarask/button_bold.png index f662c76c..d02aeeeb 100644 Binary files a/skins/common/images/be-tarask/button_bold.png and b/skins/common/images/be-tarask/button_bold.png differ diff --git a/skins/common/images/be-tarask/button_italic.png b/skins/common/images/be-tarask/button_italic.png index 88e1b3bb..d93e9f87 100644 Binary files a/skins/common/images/be-tarask/button_italic.png and b/skins/common/images/be-tarask/button_italic.png differ diff --git a/skins/common/images/be-tarask/button_link.png b/skins/common/images/be-tarask/button_link.png index f68d5d56..07bc6a19 100644 Binary files a/skins/common/images/be-tarask/button_link.png and b/skins/common/images/be-tarask/button_link.png differ diff --git a/skins/common/images/button_bold.png b/skins/common/images/button_bold.png index 5e52deed..2827dba0 100644 Binary files a/skins/common/images/button_bold.png and b/skins/common/images/button_bold.png differ diff --git a/skins/common/images/button_extlink.png b/skins/common/images/button_extlink.png index 12ec5f2e..e551e47e 100644 Binary files a/skins/common/images/button_extlink.png and b/skins/common/images/button_extlink.png differ diff --git a/skins/common/images/button_headline.png b/skins/common/images/button_headline.png index aa0ca540..9867c365 100644 Binary files a/skins/common/images/button_headline.png and b/skins/common/images/button_headline.png differ diff --git a/skins/common/images/button_hr.png b/skins/common/images/button_hr.png index 19cfc480..7f21402a 100644 Binary files a/skins/common/images/button_hr.png and b/skins/common/images/button_hr.png differ diff --git a/skins/common/images/button_image.png b/skins/common/images/button_image.png index 6c3c3308..abd47dde 100644 Binary files a/skins/common/images/button_image.png and b/skins/common/images/button_image.png differ diff --git a/skins/common/images/button_italic.png b/skins/common/images/button_italic.png index 96b1fb9f..f248fa93 100644 Binary files a/skins/common/images/button_italic.png and b/skins/common/images/button_italic.png differ diff --git a/skins/common/images/button_link.png b/skins/common/images/button_link.png index e9507b97..cc72b523 100644 Binary files a/skins/common/images/button_link.png and b/skins/common/images/button_link.png differ diff --git a/skins/common/images/button_math.png b/skins/common/images/button_math.png index e91fb613..507e4b51 100644 Binary files a/skins/common/images/button_math.png and b/skins/common/images/button_math.png differ diff --git a/skins/common/images/button_media.png b/skins/common/images/button_media.png index 02070790..56b1b239 100644 Binary files a/skins/common/images/button_media.png and b/skins/common/images/button_media.png differ diff --git a/skins/common/images/button_nowiki.png b/skins/common/images/button_nowiki.png index 7b2d5392..321a2cb4 100644 Binary files a/skins/common/images/button_nowiki.png and b/skins/common/images/button_nowiki.png differ diff --git a/skins/common/images/button_sig.png b/skins/common/images/button_sig.png index ef3a46d2..39de3b67 100644 Binary files a/skins/common/images/button_sig.png and b/skins/common/images/button_sig.png differ diff --git a/skins/common/images/button_template.png b/skins/common/images/button_template.png index 8e9cc267..ebbca3cf 100644 Binary files a/skins/common/images/button_template.png and b/skins/common/images/button_template.png differ diff --git a/skins/common/images/cyrl/button_italic.png b/skins/common/images/cyrl/button_italic.png index f5e588ec..f79170fd 100644 Binary files a/skins/common/images/cyrl/button_italic.png and b/skins/common/images/cyrl/button_italic.png differ diff --git a/skins/common/images/cyrl/button_link.png b/skins/common/images/cyrl/button_link.png index a690cb35..aa28e85a 100644 Binary files a/skins/common/images/cyrl/button_link.png and b/skins/common/images/cyrl/button_link.png differ diff --git a/skins/common/images/de/button_bold.png b/skins/common/images/de/button_bold.png index 8386828d..32a22800 100644 Binary files a/skins/common/images/de/button_bold.png and b/skins/common/images/de/button_bold.png differ diff --git a/skins/common/images/de/button_italic.png b/skins/common/images/de/button_italic.png index ec719998..875eb2a1 100644 Binary files a/skins/common/images/de/button_italic.png and b/skins/common/images/de/button_italic.png differ diff --git a/skins/common/images/fa/button_bold.png b/skins/common/images/fa/button_bold.png index 2f269a8d..49680152 100644 Binary files a/skins/common/images/fa/button_bold.png and b/skins/common/images/fa/button_bold.png differ diff --git a/skins/common/images/fa/button_headline.png b/skins/common/images/fa/button_headline.png index ef9d109f..9d62767e 100644 Binary files a/skins/common/images/fa/button_headline.png and b/skins/common/images/fa/button_headline.png differ diff --git a/skins/common/images/fa/button_italic.png b/skins/common/images/fa/button_italic.png index 84985848..fc9faf3d 100644 Binary files a/skins/common/images/fa/button_italic.png and b/skins/common/images/fa/button_italic.png differ diff --git a/skins/common/images/fa/button_link.png b/skins/common/images/fa/button_link.png index 4ad5ef56..de56a5c3 100644 Binary files a/skins/common/images/fa/button_link.png and b/skins/common/images/fa/button_link.png differ diff --git a/skins/common/images/fileicon.xcf b/skins/common/images/fileicon.xcf deleted file mode 100644 index 85a0a610..00000000 Binary files a/skins/common/images/fileicon.xcf and /dev/null differ diff --git a/skins/common/images/gnu-fdl.png b/skins/common/images/gnu-fdl.png index 1371aba8..10915329 100644 Binary files a/skins/common/images/gnu-fdl.png and b/skins/common/images/gnu-fdl.png differ diff --git a/skins/common/images/gnu-fdl.xcf b/skins/common/images/gnu-fdl.xcf deleted file mode 100644 index 364440dd..00000000 Binary files a/skins/common/images/gnu-fdl.xcf and /dev/null differ diff --git a/skins/common/images/icons/fileicon-c.png b/skins/common/images/icons/fileicon-c.png index 6da6916e..f7984fa0 100644 Binary files a/skins/common/images/icons/fileicon-c.png and b/skins/common/images/icons/fileicon-c.png differ diff --git a/skins/common/images/icons/fileicon-cpp.png b/skins/common/images/icons/fileicon-cpp.png index ba54e77f..0a9b4cbc 100644 Binary files a/skins/common/images/icons/fileicon-cpp.png and b/skins/common/images/icons/fileicon-cpp.png differ diff --git a/skins/common/images/icons/fileicon-deb.png b/skins/common/images/icons/fileicon-deb.png index ac1e2cf9..605bea17 100644 Binary files a/skins/common/images/icons/fileicon-deb.png and b/skins/common/images/icons/fileicon-deb.png differ diff --git a/skins/common/images/icons/fileicon-djvu.png b/skins/common/images/icons/fileicon-djvu.png index 2e1e2c9b..3eaca1f8 100644 Binary files a/skins/common/images/icons/fileicon-djvu.png and b/skins/common/images/icons/fileicon-djvu.png differ diff --git a/skins/common/images/icons/fileicon-dvi.png b/skins/common/images/icons/fileicon-dvi.png index 6c7aa1a1..790ec41b 100644 Binary files a/skins/common/images/icons/fileicon-dvi.png and b/skins/common/images/icons/fileicon-dvi.png differ diff --git a/skins/common/images/icons/fileicon-exe.png b/skins/common/images/icons/fileicon-exe.png index 6ccf1821..f310ad0d 100644 Binary files a/skins/common/images/icons/fileicon-exe.png and b/skins/common/images/icons/fileicon-exe.png differ diff --git a/skins/common/images/icons/fileicon-h.png b/skins/common/images/icons/fileicon-h.png index d091afff..cf158528 100644 Binary files a/skins/common/images/icons/fileicon-h.png and b/skins/common/images/icons/fileicon-h.png differ diff --git a/skins/common/images/icons/fileicon-html.png b/skins/common/images/icons/fileicon-html.png index 7c479525..1c3a1588 100644 Binary files a/skins/common/images/icons/fileicon-html.png and b/skins/common/images/icons/fileicon-html.png differ diff --git a/skins/common/images/icons/fileicon-iso.png b/skins/common/images/icons/fileicon-iso.png index b4192e9e..74b06615 100644 Binary files a/skins/common/images/icons/fileicon-iso.png and b/skins/common/images/icons/fileicon-iso.png differ diff --git a/skins/common/images/icons/fileicon-java.png b/skins/common/images/icons/fileicon-java.png index 757c6aec..730ab232 100644 Binary files a/skins/common/images/icons/fileicon-java.png and b/skins/common/images/icons/fileicon-java.png differ diff --git a/skins/common/images/icons/fileicon-mid.png b/skins/common/images/icons/fileicon-mid.png index aa826070..5254418e 100644 Binary files a/skins/common/images/icons/fileicon-mid.png and b/skins/common/images/icons/fileicon-mid.png differ diff --git a/skins/common/images/icons/fileicon-mov.png b/skins/common/images/icons/fileicon-mov.png index 2c0da0d8..37e479aa 100644 Binary files a/skins/common/images/icons/fileicon-mov.png and b/skins/common/images/icons/fileicon-mov.png differ diff --git a/skins/common/images/icons/fileicon-o.png b/skins/common/images/icons/fileicon-o.png index bf051cb8..24ac2cc5 100644 Binary files a/skins/common/images/icons/fileicon-o.png and b/skins/common/images/icons/fileicon-o.png differ diff --git a/skins/common/images/icons/fileicon-ogg.png b/skins/common/images/icons/fileicon-ogg.png index b8ba7714..c50d1ee8 100644 Binary files a/skins/common/images/icons/fileicon-ogg.png and b/skins/common/images/icons/fileicon-ogg.png differ diff --git a/skins/common/images/icons/fileicon-pdf.png b/skins/common/images/icons/fileicon-pdf.png index ee39b6c3..c195c761 100644 Binary files a/skins/common/images/icons/fileicon-pdf.png and b/skins/common/images/icons/fileicon-pdf.png differ diff --git a/skins/common/images/icons/fileicon-ps.png b/skins/common/images/icons/fileicon-ps.png index f1f504d7..342a84ee 100644 Binary files a/skins/common/images/icons/fileicon-ps.png and b/skins/common/images/icons/fileicon-ps.png differ diff --git a/skins/common/images/icons/fileicon-rm.png b/skins/common/images/icons/fileicon-rm.png index 5ba04e5a..534dbeb2 100644 Binary files a/skins/common/images/icons/fileicon-rm.png and b/skins/common/images/icons/fileicon-rm.png differ diff --git a/skins/common/images/icons/fileicon-rpm.png b/skins/common/images/icons/fileicon-rpm.png index 0f1c3b87..aab3ec39 100644 Binary files a/skins/common/images/icons/fileicon-rpm.png and b/skins/common/images/icons/fileicon-rpm.png differ diff --git a/skins/common/images/icons/fileicon-svg.png b/skins/common/images/icons/fileicon-svg.png index 8dc6d30f..16a666f3 100644 Binary files a/skins/common/images/icons/fileicon-svg.png and b/skins/common/images/icons/fileicon-svg.png differ diff --git a/skins/common/images/icons/fileicon-tar.png b/skins/common/images/icons/fileicon-tar.png index a4b15d7f..7266b5b5 100644 Binary files a/skins/common/images/icons/fileicon-tar.png and b/skins/common/images/icons/fileicon-tar.png differ diff --git a/skins/common/images/icons/fileicon-tex.png b/skins/common/images/icons/fileicon-tex.png index ee8c0226..55187918 100644 Binary files a/skins/common/images/icons/fileicon-tex.png and b/skins/common/images/icons/fileicon-tex.png differ diff --git a/skins/common/images/icons/fileicon-ttf.png b/skins/common/images/icons/fileicon-ttf.png index 1b53066e..13bd7ced 100644 Binary files a/skins/common/images/icons/fileicon-ttf.png and b/skins/common/images/icons/fileicon-ttf.png differ diff --git a/skins/common/images/icons/fileicon-txt.png b/skins/common/images/icons/fileicon-txt.png index 76e98909..bec58b4e 100644 Binary files a/skins/common/images/icons/fileicon-txt.png and b/skins/common/images/icons/fileicon-txt.png differ diff --git a/skins/common/images/ksh/button_S_italic.png b/skins/common/images/ksh/button_S_italic.png index 0761a1e1..00000a3c 100644 Binary files a/skins/common/images/ksh/button_S_italic.png and b/skins/common/images/ksh/button_S_italic.png differ diff --git a/skins/common/images/link_icon.gif b/skins/common/images/link_icon.gif index 815ccb1b..168c1a2f 100644 Binary files a/skins/common/images/link_icon.gif and b/skins/common/images/link_icon.gif differ diff --git a/skins/common/images/magnify-clip.png b/skins/common/images/magnify-clip.png index 50abcb68..00a9cee1 100644 Binary files a/skins/common/images/magnify-clip.png and b/skins/common/images/magnify-clip.png differ diff --git a/skins/common/images/mediawiki-small.xcf b/skins/common/images/mediawiki-small.xcf deleted file mode 100644 index 75355171..00000000 Binary files a/skins/common/images/mediawiki-small.xcf and /dev/null differ diff --git a/skins/common/images/mediawiki.png b/skins/common/images/mediawiki.png index 69fce988..0f35886a 100644 Binary files a/skins/common/images/mediawiki.png and b/skins/common/images/mediawiki.png differ diff --git a/skins/common/images/poweredby_mediawiki_88x31.png b/skins/common/images/poweredby_mediawiki_88x31.png index ce1765d1..3714414f 100644 Binary files a/skins/common/images/poweredby_mediawiki_88x31.png and b/skins/common/images/poweredby_mediawiki_88x31.png differ diff --git a/skins/common/images/public-domain.png b/skins/common/images/public-domain.png index e5fb33ce..f51f5602 100644 Binary files a/skins/common/images/public-domain.png and b/skins/common/images/public-domain.png differ diff --git a/skins/common/images/redirectltr.png b/skins/common/images/redirectltr.png index 9110ca13..bcf5742c 100644 Binary files a/skins/common/images/redirectltr.png and b/skins/common/images/redirectltr.png differ diff --git a/skins/common/images/redirectrtl.png b/skins/common/images/redirectrtl.png index 60fd59d3..8d99841a 100644 Binary files a/skins/common/images/redirectrtl.png and b/skins/common/images/redirectrtl.png differ diff --git a/skins/common/images/remove.png b/skins/common/images/remove.png new file mode 100644 index 00000000..0cbf7d73 Binary files /dev/null and b/skins/common/images/remove.png differ diff --git a/skins/common/images/sort_down.gif b/skins/common/images/sort_down.gif index 5ff08160..d97e8285 100644 Binary files a/skins/common/images/sort_down.gif and b/skins/common/images/sort_down.gif differ diff --git a/skins/common/images/sort_none.gif b/skins/common/images/sort_none.gif index 6bb02824..edd07e58 100644 Binary files a/skins/common/images/sort_none.gif and b/skins/common/images/sort_none.gif differ diff --git a/skins/common/images/sort_up.gif b/skins/common/images/sort_up.gif index 53002968..488cf279 100644 Binary files a/skins/common/images/sort_up.gif and b/skins/common/images/sort_up.gif differ diff --git a/skins/common/images/spinner.gif b/skins/common/images/spinner.gif index bdd59d52..37d3a43d 100644 Binary files a/skins/common/images/spinner.gif and b/skins/common/images/spinner.gif differ diff --git a/skins/common/images/wiki.png b/skins/common/images/wiki.png index 49913f6a..4f1dc263 100644 Binary files a/skins/common/images/wiki.png and b/skins/common/images/wiki.png differ diff --git a/skins/common/jquery.js b/skins/common/jquery.js new file mode 100644 index 00000000..c25ee31c --- /dev/null +++ b/skins/common/jquery.js @@ -0,0 +1,4384 @@ +/*! + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){ + +var + // Will speed up references to window, and allows munging its name. + window = this, + // Will speed up references to undefined, and allows munging its name. + undefined, + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + // Map over the $ in case of overwrite + _$ = window.$, + + jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + this.context = selector; + return this; + } + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem && elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + var ret = jQuery( elem || [] ); + ret.context = document; + ret.selector = selector; + return ret; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document ).ready( selector ); + + // Make sure that old selector state is passed along + if ( selector.selector && selector.context ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return this.setArray(jQuery.isArray( selector ) ? + selector : + jQuery.makeArray(selector)); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.3.2", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num === undefined ? + + // Return a 'clean' array + Array.prototype.slice.call( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) + ret.selector = this.selector + (this.selector ? " " : "") + selector; + else if ( name ) + ret.selector = this.selector + "." + name + "(" + selector + ")"; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( typeof name === "string" ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text !== "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).clone(); + + if ( this[0].parentNode ) + wrap.insertBefore( this[0] ); + + wrap.map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }).append(this); + } + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: [].push, + sort: [].sort, + splice: [].splice, + + find: function( selector ) { + if ( this.length === 1 ) { + var ret = this.pushStack( [], "find", selector ); + ret.length = 0; + jQuery.find( selector, this[0], ret ); + return ret; + } else { + return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + })), "find", selector ); + } + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var html = this.outerHTML; + if ( !html ) { + var div = this.ownerDocument.createElement("div"); + div.appendChild( this.cloneNode(true) ); + html = div.innerHTML; + } + + return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; + } else + return this.cloneNode(true); + }); + + // Copy the events from the original to the clone + if ( events === true ) { + var orig = this.find("*").andSelf(), i = 0; + + ret.find("*").andSelf().each(function(){ + if ( this.nodeName !== orig[i].nodeName ) + return; + + var events = jQuery.data( orig[i], "events" ); + + for ( var type in events ) { + for ( var handler in events[ type ] ) { + jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); + } + } + + i++; + }); + } + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ + return elem.nodeType === 1; + }) ), "filter", selector ); + }, + + closest: function( selector ) { + var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, + closer = 0; + + return this.map(function(){ + var cur = this; + while ( cur && cur.ownerDocument ) { + if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { + jQuery.data(cur, "closest", closer); + return cur; + } + cur = cur.parentNode; + closer++; + } + }); + }, + + not: function( selector ) { + if ( typeof selector === "string" ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector === "string" ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return !!selector && this.is( "." + selector ); + }, + + val: function( value ) { + if ( value === undefined ) { + var elem = this[0]; + + if ( elem ) { + if( jQuery.nodeName( elem, 'option' ) ) + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if ( typeof value === "number" ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value === undefined ? + (this[0] ? + this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, +i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ), + "slice", Array.prototype.slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + domManip: function( args, table, callback ) { + if ( this[0] ) { + var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), + scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), + first = fragment.firstChild; + + if ( first ) + for ( var i = 0, l = this.length; i < l; i++ ) + callback.call( root(this[i], first), this.length > 1 || i > 0 ? + fragment.cloneNode(true) : fragment ); + + if ( scripts ) + jQuery.each( scripts, evalScript ); + } + + return this; + + function root( elem, cur ) { + return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; + } + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy === "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +// exclude the following css properties to add px +var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}, + toString = Object.prototype.toString; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return toString.call(obj) === "[object Function]"; + }, + + isArray: function( obj ) { + return toString.call(obj) === "[object Array]"; + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && /\S/.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.support.scriptEval ) + script.appendChild( document.createTextNode( data ) ); + else + script.text = data; + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length === undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length === undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames !== undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force, extra ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + + if ( extra === "border" ) + return; + + jQuery.each( which, function() { + if ( !extra ) + val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + if ( extra === "margin" ) + val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; + else + val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + } + + if ( elem.offsetWidth !== 0 ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, Math.round(val)); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // We need to handle opacity special in IE + if ( name == "opacity" && !jQuery.support.opacity ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + try{ + var computedStyle = defaultView.getComputedStyle( elem, null ); + }catch(e){ + // Error in getting computedStyle + } + if ( computedStyle ) + ret = computedStyle.getPropertyValue( name ); + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context, fragment ) { + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { + var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); + if ( match ) + return [ context.createElement( match[1] ) ]; + } + + var ret = [], scripts = [], div = context.createElement("div"); + + jQuery.each(elems, function(i, elem){ + if ( typeof elem === "number" ) + elem += ''; + + if ( !elem ) + return; + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
    " ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and - - {if jsvarurl {}} - {if pagecss {}} - {usercss} - {sitecss} - {gencss} - {if userjs {}} - {if userjsprev {}} - {trackbackhtml} - - -
    -
    -
    - - {if sitenotice {
    {sitenotice}
    }} -

    {title}

    -
    -

    {msg {tagline}}

    -
    {subtitle}
    - {if undelete {
    {undelete}
    }} - {if newtalk {
    {newtalk}
    }} - {if showjumplinks { - - }} - - {bodytext} - {if catlinks { }} - -
    -
    -
    -
    -
    -
    -
    {msg {views}}
    - -
    -
    -
    {msg {personaltools}}
    -
    -
      - {personal_urls {
    • $text
    • }} -
    -
    -
    - - - {sidebar { -
    -
    $barname
    -
    -
      - } { -
    -
    -
    - } {
  • $text
  • - } - } - - -
    -
    {msg {toolbox}}
    -
    - -
    -
    - {language_urls { -
    -
    {msg {otherlanguages}}
    -
    -
      - $body -
    -
    -
    - } { -
  • $text
  • - }} -
    -
    - - -
    -{reporttime} -{if {} { vim: set syn=html ts=2 : }} - diff --git a/skins/disabled/MonoBookCBT.php b/skins/disabled/MonoBookCBT.php deleted file mode 100644 index 0d693a86..00000000 --- a/skins/disabled/MonoBookCBT.php +++ /dev/null @@ -1,1389 +0,0 @@ -execute( $out ); - } - - function execute( &$out ) { - global $wgTitle, $wgStyleDirectory, $wgParserCacheType; - $fname = 'SkinMonoBookCBT::execute'; - wfProfileIn( $fname ); - wfProfileIn( "$fname-setup" ); - Skin::initPage( $out ); - - $this->mOut =& $out; - $this->mTitle =& $wgTitle; - - $sourceFile = "$wgStyleDirectory/MonoBook.tpl"; - - wfProfileOut( "$fname-setup" ); - - if ( $wgParserCacheType == CACHE_NONE ) { - $template = file_get_contents( $sourceFile ); - $text = $this->executeTemplate( $template ); - } else { - $compiled = $this->getCompiledTemplate( $sourceFile ); - - wfProfileIn( "$fname-eval" ); - $text = eval( $compiled ); - wfProfileOut( "$fname-eval" ); - } - wfProfileOut( $fname ); - return $text; - } - - function getCompiledTemplate( $sourceFile ) { - global $wgDBname, $wgMemc, $wgRequest, $wgUser, $parserMemc; - $fname = 'SkinMonoBookCBT::getCompiledTemplate'; - - $expiry = 3600; - - // Sandbox template execution - if ( $this->mCompiling ) { - return; - } - - wfProfileIn( $fname ); - - // Is the request an ordinary page view? - if ( $wgRequest->wasPosted() || - count( array_diff( array_keys( $_GET ), array( 'title', 'useskin', 'recompile' ) ) ) != 0 ) - { - $type = 'nonview'; - } else { - $type = 'view'; - } - - // Per-user compiled template - // Put all logged-out users on the same cache key - $cacheKey = "$wgDBname:monobookcbt:$type:" . $wgUser->getId(); - - $recompile = $wgRequest->getVal( 'recompile' ); - if ( $recompile == 'user' ) { - $recompileUser = true; - $recompileGeneric = false; - } elseif ( $recompile ) { - $recompileUser = true; - $recompileGeneric = true; - } else { - $recompileUser = false; - $recompileGeneric = false; - } - - if ( !$recompileUser ) { - $php = $parserMemc->get( $cacheKey ); - } - if ( $recompileUser || !$php ) { - if ( $wgUser->isLoggedIn() ) { - // Perform staged compilation - // First compile a generic template for all logged-in users - $genericKey = "$wgDBname:monobookcbt:$type:loggedin"; - if ( !$recompileGeneric ) { - $template = $parserMemc->get( $genericKey ); - } - if ( $recompileGeneric || !$template ) { - $template = file_get_contents( $sourceFile ); - $ignore = array( 'loggedin', '!loggedin dynamic' ); - if ( $type == 'view' ) { - $ignore[] = 'nonview dynamic'; - } - $template = $this->compileTemplate( $template, $ignore ); - $parserMemc->set( $genericKey, $template, $expiry ); - } - } else { - $template = file_get_contents( $sourceFile ); - } - - $ignore = array( 'lang', 'loggedin', 'user' ); - if ( $wgUser->isLoggedIn() ) { - $ignore[] = '!loggedin dynamic'; - } else { - $ignore[] = 'loggedin dynamic'; - } - if ( $type == 'view' ) { - $ignore[] = 'nonview dynamic'; - } - $compiled = $this->compileTemplate( $template, $ignore ); - - // Reduce whitespace - // This is done here instead of in CBTProcessor because we can be - // more sure it is safe here. - $compiled = preg_replace( '/^[ \t]+/m', '', $compiled ); - $compiled = preg_replace( '/[\r\n]+/', "\n", $compiled ); - - // Compile to PHP - $compiler = new CBTCompiler( $compiled ); - $ret = $compiler->compile(); - if ( $ret !== true ) { - echo $ret; - wfErrorExit(); - } - $php = $compiler->generatePHP( '$this' ); - - $parserMemc->set( $cacheKey, $php, $expiry ); - } - wfProfileOut( $fname ); - return $php; - } - - function compileTemplate( $template, $ignore ) { - $tp = new CBTProcessor( $template, $this, $ignore ); - $tp->mFunctionCache = $this->mFunctionCache; - - $this->mCompiling = true; - $compiled = $tp->compile(); - $this->mCompiling = false; - - if ( $tp->getLastError() ) { - // If there was a compile error, don't save the template - // Instead just print the error and exit - echo $compiled; - wfErrorExit(); - } - $this->mFunctionCache = $tp->mFunctionCache; - return $compiled; - } - - function executeTemplate( $template ) { - $fname = 'SkinMonoBookCBT::executeTemplate'; - wfProfileIn( $fname ); - $tp = new CBTProcessor( $template, $this ); - $tp->mFunctionCache = $this->mFunctionCache; - - $this->mCompiling = true; - $text = $tp->execute(); - $this->mCompiling = false; - - $this->mFunctionCache = $tp->mFunctionCache; - wfProfileOut( $fname ); - return $text; - } - - /****************************************************** - * Callbacks * - ******************************************************/ - - function lang() { return $GLOBALS['wgContLanguageCode']; } - - function dir() { - global $wgContLang; - return $wgContLang->isRTL() ? 'rtl' : 'ltr'; - } - - function mimetype() { return $GLOBALS['wgMimeType']; } - function charset() { return $GLOBALS['wgOutputEncoding']; } - function headlinks() { - return cbt_value( $this->mOut->getHeadLinks(), 'dynamic' ); - } - function headscripts() { - return cbt_value( $this->mOut->getScript(), 'dynamic' ); - } - - function pagetitle() { - return cbt_value( $this->mOut->getHTMLTitle(), array( 'title', 'lang' ) ); - } - - function stylepath() { return $GLOBALS['wgStylePath']; } - function stylename() { return $this->mStyleName; } - - function notprintable() { - global $wgRequest; - return cbt_value( !$wgRequest->getBool( 'printable' ), 'nonview dynamic' ); - } - - function jsmimetype() { return $GLOBALS['wgJsMimeType']; } - - function jsvarurl() { - global $wgUseSiteJs, $wgUser; - if ( !$wgUseSiteJs ) return ''; - - if ( $wgUser->isLoggedIn() ) { - $url = self::makeUrl( '-','action=raw&smaxage=0&gen=js' ); - } else { - $url = self::makeUrl( '-','action=raw&gen=js' ); - } - return cbt_value( $url, 'loggedin' ); - } - - function pagecss() { - global $wgHooks; - - $out = false; - wfRunHooks( 'SkinTemplateSetupPageCss', array( &$out ) ); - - // Unknown dependencies - return cbt_value( $out, 'dynamic' ); - } - - function usercss() { - if ( $this->isCssPreview() ) { - global $wgRequest; - $usercss = $this->makeStylesheetCdata( $wgRequest->getText('wpTextbox1') ); - } else { - $usercss = $this->makeStylesheetLink( self::makeUrl($this->getUserPageText() . - '/'.$this->mStyleName.'.css', 'action=raw&ctype=text/css' ) ); - } - - // Dynamic when not an ordinary page view, also depends on the username - return cbt_value( $usercss, array( 'nonview dynamic', 'user' ) ); - } - - function sitecss() { - global $wgUseSiteCss; - if ( !$wgUseSiteCss ) { - return ''; - } - - global $wgSquidMaxage, $wgContLang, $wgStylePath; - - $query = "action=raw&ctype=text/css&smaxage=$wgSquidMaxage"; - - $sitecss = ''; - if ( $wgContLang->isRTL() ) { - $sitecss .= $this->makeStylesheetLink( $wgStylePath . '/' . $this->mStyleName . '/rtl.css' ) . "\n"; - } - - $sitecss .= $this->makeStylesheetLink( self::makeNSUrl( 'Common.css', $query, NS_MEDIAWIKI ) ) . "\n"; - $sitecss .= $this->makeStylesheetLink( self::makeNSUrl( ucfirst( $this->mStyleName ) . '.css', $query, NS_MEDIAWIKI ) ) . "\n"; - - // No deps - return $sitecss; - } - - function gencss() { - global $wgUseSiteCss; - if ( !$wgUseSiteCss ) return ''; - - global $wgSquidMaxage, $wgUser, $wgAllowUserCss; - if ( $this->isCssPreview() ) { - $siteargs = '&smaxage=0&maxage=0'; - } else { - $siteargs = '&maxage=' . $wgSquidMaxage; - } - if ( $wgAllowUserCss && $wgUser->isLoggedIn() ) { - $siteargs .= '&ts={user_touched}'; - $isTemplate = true; - } else { - $isTemplate = false; - } - - $link = $this->makeStylesheetLink( self::makeUrl('-','action=raw&gen=css' . $siteargs) ) . "\n"; - - if ( $wgAllowUserCss ) { - $deps = 'loggedin'; - } else { - $deps = array(); - } - return cbt_value( $link, $deps, $isTemplate ); - } - - function user_touched() { - global $wgUser; - return cbt_value( $wgUser->mTouched, 'dynamic' ); - } - - function userjs() { - global $wgAllowUserJs, $wgJsMimeType; - if ( !$wgAllowUserJs ) return ''; - - if ( $this->isJsPreview() ) { - $url = ''; - } else { - $url = self::makeUrl($this->getUserPageText().'/'.$this->mStyleName.'.js', 'action=raw&ctype='.$wgJsMimeType.'&dontcountme=s'); - } - return cbt_value( $url, array( 'nonview dynamic', 'user' ) ); - } - - function userjsprev() { - global $wgAllowUserJs, $wgRequest; - if ( !$wgAllowUserJs ) return ''; - if ( $this->isJsPreview() ) { - $js = '/*getText('wpTextbox1') . ' /*]]>*/'; - } else { - $js = ''; - } - return cbt_value( $js, array( 'nonview dynamic' ) ); - } - - function trackbackhtml() { - global $wgUseTrackbacks; - if ( !$wgUseTrackbacks ) return ''; - - if ( $this->mOut->isArticleRelated() ) { - $tb = $this->mTitle->trackbackRDF(); - } else { - $tb = ''; - } - return cbt_value( $tb, 'dynamic' ); - } - - function body_ondblclick() { - global $wgUser; - if( $this->isEditable() && $wgUser->getOption("editondblclick") ) { - $js = 'document.location = "' . $this->getEditUrl() .'";'; - } else { - $js = ''; - } - - if ( User::getDefaultOption('editondblclick') ) { - return cbt_value( $js, 'user', 'title' ); - } else { - // Optimise away for logged-out users - return cbt_value( $js, 'loggedin dynamic' ); - } - } - - function body_onload() { - global $wgUser; - if ( $this->isEditable() && $wgUser->getOption( 'editsectiononrightclick' ) ) { - $js = 'setupRightClickEdit()'; - } else { - $js = ''; - } - return cbt_value( $js, 'loggedin dynamic' ); - } - - function nsclass() { - return cbt_value( 'ns-' . $this->mTitle->getNamespace(), 'title' ); - } - - function sitenotice() { - // Perhaps this could be given special dependencies using our knowledge of what - // wfGetSiteNotice() depends on. - return cbt_value( wfGetSiteNotice(), 'dynamic' ); - } - - function title() { - return cbt_value( $this->mOut->getPageTitle(), array( 'title', 'lang' ) ); - } - - function title_urlform() { - return cbt_value( $this->getThisTitleUrlForm(), 'title' ); - } - - function title_userurl() { - return cbt_value( urlencode( $this->mTitle->getDBkey() ), 'title' ); - } - - function subtitle() { - $subpagestr = $this->subPageSubtitle(); - if ( !empty( $subpagestr ) ) { - $s = ''.$subpagestr.''.$this->mOut->getSubtitle(); - } else { - $s = $this->mOut->getSubtitle(); - } - return cbt_value( $s, array( 'title', 'nonview dynamic' ) ); - } - - function undelete() { - return cbt_value( $this->getUndeleteLink(), array( 'title', 'lang' ) ); - } - - function newtalk() { - global $wgUser, $wgDBname; - $newtalks = $wgUser->getNewMessageLinks(); - - if (count($newtalks) == 1 && $newtalks[0]["wiki"] === $wgDBname) { - $usertitle = $this->getUserPageTitle(); - $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' - ) - ); - # Disable Cache - $this->mOut->setSquidMaxage(0); - } - } else if (count($newtalks)) { - $sep = str_replace("_", " ", wfMsgHtml("newtalkseparator")); - $msgs = array(); - foreach ($newtalks as $newtalk) { - $msgs[] = wfElement("a", - array('href' => $newtalk["link"]), $newtalk["wiki"]); - } - $parts = implode($sep, $msgs); - $ntl = wfMsgHtml('youhavenewmessagesmulti', $parts); - $this->mOut->setSquidMaxage(0); - } else { - $ntl = ''; - } - return cbt_value( $ntl, 'dynamic' ); - } - - function showjumplinks() { - global $wgUser; - return cbt_value( $wgUser->getOption( 'showjumplinks' ) ? 'true' : '', 'user' ); - } - - function bodytext() { - return cbt_value( $this->mOut->getHTML(), 'dynamic' ); - } - - function catlinks() { - if ( !isset( $this->mCatlinks ) ) { - $this->mCatlinks = $this->getCategories(); - } - return cbt_value( $this->mCatlinks, 'dynamic' ); - } - - function extratabs( $itemTemplate ) { - global $wgContLang, $wgDisableLangConversion; - - $etpl = cbt_escape( $itemTemplate ); - - /* show links to different language variants */ - $variants = $wgContLang->getVariants(); - $s = ''; - if ( !$wgDisableLangConversion && count( $wgContLang->getVariants() ) > 1 ) { - $vcount=0; - foreach ( $variants as $code ) { - $name = $wgContLang->getVariantname( $code ); - if ( $name == 'disable' ) { - continue; - } - $code = cbt_escape( $code ); - $name = cbt_escape( $name ); - $s .= "{ca_variant {{$code}} {{$name}} {{$vcount}} {{$etpl}}}\n"; - $vcount ++; - } - } - return cbt_value( $s, array(), true ); - } - - function is_special() { return cbt_value( $this->mTitle->getNamespace() == NS_SPECIAL, 'title' ); } - function can_edit() { return cbt_value( (string)($this->mTitle->userCan( 'edit' )), 'dynamic' ); } - function can_move() { return cbt_value( (string)($this->mTitle->userCan( 'move' )), 'dynamic' ); } - function is_talk() { return cbt_value( (string)($this->mTitle->isTalkPage()), 'title' ); } - function is_protected() { return cbt_value( (string)$this->mTitle->isProtected(), 'dynamic' ); } - function nskey() { return cbt_value( $this->mTitle->getNamespaceKey(), 'title' ); } - - function request_url() { - global $wgRequest; - return cbt_value( $wgRequest->getRequestURL(), 'dynamic' ); - } - - function subject_url() { - $title = $this->getSubjectPage(); - if ( $title->exists() ) { - $url = $title->getLocalUrl(); - } else { - $url = $title->getLocalUrl( 'action=edit' ); - } - return cbt_value( $url, 'title' ); - } - - function talk_url() { - $title = $this->getTalkPage(); - if ( $title->exists() ) { - $url = $title->getLocalUrl(); - } else { - $url = $title->getLocalUrl( 'action=edit' ); - } - return cbt_value( $url, 'title' ); - } - - function edit_url() { - return cbt_value( $this->getEditUrl(), array( 'title', 'nonview dynamic' ) ); - } - - function move_url() { - return cbt_value( $this->makeSpecialParamUrl( 'Movepage' ), array(), true ); - } - - function localurl( $query ) { - return cbt_value( $this->mTitle->getLocalURL( $query ), 'title' ); - } - - function selecttab( $tab, $extraclass = '' ) { - if ( !isset( $this->mSelectedTab ) ) { - $prevent_active_tabs = false ; - wfRunHooks( 'SkinTemplatePreventOtherActiveTabs', array( &$this , &$preventActiveTabs ) ); - - $actionTabs = array( - 'edit' => 'edit', - 'submit' => 'edit', - 'history' => 'history', - 'protect' => 'protect', - 'unprotect' => 'protect', - 'delete' => 'delete', - 'watch' => 'watch', - 'unwatch' => 'watch', - ); - if ( $preventActiveTabs ) { - $this->mSelectedTab = false; - } else { - $action = $this->getAction(); - $section = $this->getSection(); - - if ( isset( $actionTabs[$action] ) ) { - $this->mSelectedTab = $actionTabs[$action]; - - if ( $this->mSelectedTab == 'edit' && $section == 'new' ) { - $this->mSelectedTab = 'addsection'; - } - } elseif ( $this->mTitle->isTalkPage() ) { - $this->mSelectedTab = 'talk'; - } else { - $this->mSelectedTab = 'subject'; - } - } - } - if ( $extraclass ) { - if ( $this->mSelectedTab == $tab ) { - $s = 'class="selected ' . htmlspecialchars( $extraclass ) . '"'; - } else { - $s = 'class="' . htmlspecialchars( $extraclass ) . '"'; - } - } else { - if ( $this->mSelectedTab == $tab ) { - $s = 'class="selected"'; - } else { - $s = ''; - } - } - return cbt_value( $s, array( 'nonview dynamic', 'title' ) ); - } - - function subject_newclass() { - $title = $this->getSubjectPage(); - $class = $title->exists() ? '' : 'new'; - return cbt_value( $class, 'dynamic' ); - } - - function talk_newclass() { - $title = $this->getTalkPage(); - $class = $title->exists() ? '' : 'new'; - return cbt_value( $class, 'dynamic' ); - } - - function ca_variant( $code, $name, $index, $template ) { - global $wgContLang; - $selected = ($code == $wgContLang->getPreferredVariant()); - $action = $this->getAction(); - $actstr = ''; - if( $action ) - $actstr = 'action=' . $action . '&'; - $s = strtr( $template, array( - '$id' => htmlspecialchars( 'varlang-' . $index ), - '$class' => $selected ? 'class="selected"' : '', - '$text' => $name, - '$href' => htmlspecialchars( $this->mTitle->getLocalUrl( $actstr . 'variant=' . $code ) ) - )); - return cbt_value( $s, 'dynamic' ); - } - - function is_watching() { - return cbt_value( (string)$this->mTitle->userIsWatching(), array( 'dynamic' ) ); - } - - - function personal_urls( $itemTemplate ) { - global $wgShowIPinHeader, $wgContLang; - - # Split this function up into many small functions, to obtain the - # best specificity in the dependencies of each one. The template below - # has no dependencies, so its generation, and any static subfunctions, - # can be optimised away. - $etpl = cbt_escape( $itemTemplate ); - $s = " - {userpage {{$etpl}}} - {mytalk {{$etpl}}} - {preferences {{$etpl}}} - {watchlist {{$etpl}}} - {mycontris {{$etpl}}} - {logout {{$etpl}}} - "; - - if ( $wgShowIPinHeader ) { - $s .= " - {anonuserpage {{$etpl}}} - {anontalk {{$etpl}}} - {anonlogin {{$etpl}}} - "; - } else { - $s .= "{login {{$etpl}}}\n"; - } - // No dependencies - return cbt_value( $s, array(), true /*this is a template*/ ); - } - - function userpage( $itemTemplate ) { - global $wgUser; - if ( $this->isLoggedIn() ) { - $userPage = $this->getUserPageTitle(); - $s = $this->makeTemplateLink( $itemTemplate, 'userpage', $userPage, $wgUser->getName() ); - } else { - $s = ''; - } - return cbt_value( $s, 'user' ); - } - - function mytalk( $itemTemplate ) { - global $wgUser; - if ( $this->isLoggedIn() ) { - $userPage = $this->getUserPageTitle(); - $talkPage = $userPage->getTalkPage(); - $s = $this->makeTemplateLink( $itemTemplate, 'mytalk', $talkPage, wfMsg('mytalk') ); - } else { - $s = ''; - } - return cbt_value( $s, 'user' ); - } - - function preferences( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = $this->makeSpecialTemplateLink( $itemTemplate, 'preferences', - 'Preferences', wfMsg( 'preferences' ) ); - } else { - $s = ''; - } - return cbt_value( $s, array( 'loggedin', 'lang' ) ); - } - - function watchlist( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = $this->makeSpecialTemplateLink( $itemTemplate, 'watchlist', - 'Watchlist', wfMsg( 'watchlist' ) ); - } else { - $s = ''; - } - return cbt_value( $s, array( 'loggedin', 'lang' ) ); - } - - function mycontris( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - global $wgUser; - $s = $this->makeSpecialTemplateLink( $itemTemplate, 'mycontris', - "Contributions/" . $wgUser->getTitleKey(), wfMsg('mycontris') ); - } else { - $s = ''; - } - return cbt_value( $s, 'user' ); - } - - function logout( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = $this->makeSpecialTemplateLink( $itemTemplate, 'logout', - 'Userlogout', wfMsg( 'userlogout' ), - $this->mTitle->getNamespace() === NS_SPECIAL && $this->mTitle->getText() === 'Preferences' - ? '' : "returnto=" . $this->mTitle->getPrefixedURL() ); - } else { - $s = ''; - } - return cbt_value( $s, 'loggedin dynamic' ); - } - - function anonuserpage( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = ''; - } else { - global $wgUser; - $userPage = $this->getUserPageTitle(); - $s = $this->makeTemplateLink( $itemTemplate, 'userpage', $userPage, $wgUser->getName() ); - } - return cbt_value( $s, '!loggedin dynamic' ); - } - - function anontalk( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = ''; - } else { - $userPage = $this->getUserPageTitle(); - $talkPage = $userPage->getTalkPage(); - $s = $this->makeTemplateLink( $itemTemplate, 'mytalk', $talkPage, wfMsg('anontalk') ); - } - return cbt_value( $s, '!loggedin dynamic' ); - } - - function anonlogin( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = ''; - } else { - $s = $this->makeSpecialTemplateLink( $itemTemplate, 'anonlogin', 'Userlogin', - wfMsg( 'userlogin' ), 'returnto=' . urlencode( $this->getThisPDBK() ) ); - } - return cbt_value( $s, '!loggedin dynamic' ); - } - - function login( $itemTemplate ) { - if ( $this->isLoggedIn() ) { - $s = ''; - } else { - $s = $this->makeSpecialTemplateLink( $itemTemplate, 'login', 'Userlogin', - wfMsg( 'userlogin' ), 'returnto=' . urlencode( $this->getThisPDBK() ) ); - } - return cbt_value( $s, '!loggedin dynamic' ); - } - - function logopath() { return $GLOBALS['wgLogo']; } - function mainpage() { return self::makeMainPageUrl(); } - - function sidebar( $startSection, $endSection, $innerTpl ) { - $s = ''; - $lines = explode( "\n", wfMsgForContent( 'sidebar' ) ); - $firstSection = true; - foreach ($lines as $line) { - if (strpos($line, '*') !== 0) - continue; - if (strpos($line, '**') !== 0) { - $bar = trim($line, '* '); - $name = wfMsg( $bar ); - if (wfEmptyMsg($bar, $name)) { - $name = $bar; - } - if ( $firstSection ) { - $firstSection = false; - } else { - $s .= $endSection; - } - $s .= strtr( $startSection, - array( - '$bar' => htmlspecialchars( $bar ), - '$barname' => $name - ) ); - } else { - if (strpos($line, '|') !== false) { // sanity check - $line = explode( '|' , trim($line, '* '), 2 ); - $link = wfMsgForContent( $line[0] ); - if ($link == '-') - continue; - if (wfEmptyMsg($line[1], $text = wfMsg($line[1]))) - $text = $line[1]; - if (wfEmptyMsg($line[0], $link)) - $link = $line[0]; - $href = self::makeInternalOrExternalUrl( $link ); - - $s .= strtr( $innerTpl, - array( - '$text' => htmlspecialchars( $text ), - '$href' => htmlspecialchars( $href ), - '$id' => htmlspecialchars( 'n-' . strtr($line[1], ' ', '-') ), - '$classactive' => '' - ) ); - } else { continue; } - } - } - if ( !$firstSection ) { - $s .= $endSection; - } - - // Depends on user language only - return cbt_value( $s, 'lang' ); - } - - function searchaction() { - // Static link - return $this->getSearchLink(); - } - - function search() { - global $wgRequest; - return cbt_value( trim( $this->getSearch() ), 'special dynamic' ); - } - - function notspecialpage() { - return cbt_value( $this->mTitle->getNamespace() != NS_SPECIAL, 'special' ); - } - - function nav_whatlinkshere() { - return cbt_value( $this->makeSpecialParamUrl('Whatlinkshere' ), array(), true ); - } - - function article_exists() { - return cbt_value( (string)($this->mTitle->getArticleId() !== 0), 'title' ); - } - - function nav_recentchangeslinked() { - return cbt_value( $this->makeSpecialParamUrl('Recentchangeslinked' ), array(), true ); - } - - function feeds( $itemTemplate = '' ) { - if ( !$this->mOut->isSyndicated() ) { - $feeds = ''; - } elseif ( $itemTemplate == '' ) { - // boolean only required - $feeds = 'true'; - } else { - $feeds = ''; - global $wgFeedClasses, $wgRequest; - foreach( $wgFeedClasses as $format => $class ) { - $feeds .= strtr( $itemTemplate, - array( - '$key' => htmlspecialchars( $format ), - '$text' => $format, - '$href' => $wgRequest->appendQuery( "feed=$format" ) - ) ); - } - } - return cbt_value( $feeds, 'special dynamic' ); - } - - function is_userpage() { - list( $id, $ip ) = $this->getUserPageIdIp(); - return cbt_value( (string)($id || $ip), 'title' ); - } - - function is_ns_mediawiki() { - return cbt_value( (string)$this->mTitle->getNamespace() == NS_MEDIAWIKI, 'title' ); - } - - function is_loggedin() { - global $wgUser; - return cbt_value( (string)($wgUser->isLoggedIn()), 'loggedin' ); - } - - function nav_contributions() { - $url = $this->makeSpecialParamUrl( 'Contributions', '', '{title_userurl}' ); - return cbt_value( $url, array(), true ); - } - - function is_allowed( $right ) { - global $wgUser; - return cbt_value( (string)$wgUser->isAllowed( $right ), 'user' ); - } - - function nav_blockip() { - $url = $this->makeSpecialParamUrl( 'Blockip', '', '{title_userurl}' ); - return cbt_value( $url, array(), true ); - } - - function nav_emailuser() { - global $wgEnableEmail, $wgEnableUserEmail, $wgUser; - if ( !$wgEnableEmail || !$wgEnableUserEmail ) return ''; - - $url = $this->makeSpecialParamUrl( 'Emailuser', '', '{title_userurl}' ); - return cbt_value( $url, array(), true ); - } - - function nav_upload() { - global $wgEnableUploads, $wgUploadNavigationUrl; - if ( !$wgEnableUploads ) { - return ''; - } elseif ( $wgUploadNavigationUrl ) { - return $wgUploadNavigationUrl; - } else { - return self::makeSpecialUrl('Upload'); - } - } - - function nav_specialpages() { - return self::makeSpecialUrl('Specialpages'); - } - - function nav_print() { - global $wgRequest, $wgArticle; - $action = $this->getAction(); - $url = ''; - if( $this->mTitle->getNamespace() !== NS_SPECIAL - && ($action == '' || $action == 'view' || $action == 'purge' ) ) - { - $revid = $wgArticle->getLatest(); - if ( $revid != 0 ) { - $url = $wgRequest->appendQuery( 'printable=yes' ); - } - } - return cbt_value( $url, array( 'nonview dynamic', 'title' ) ); - } - - function nav_permalink() { - $url = (string)$this->getPermalink(); - return cbt_value( $url, 'dynamic' ); - } - - function nav_trackbacklink() { - global $wgUseTrackbacks; - if ( !$wgUseTrackbacks ) return ''; - - return cbt_value( $this->mTitle->trackbackURL(), 'title' ); - } - - function is_permalink() { - return cbt_value( (string)($this->getPermalink() === false), 'nonview dynamic' ); - } - - function toolboxend() { - // This is where the MonoBookTemplateToolboxEnd hook went in the old skin - return ''; - } - - function language_urls( $outer, $inner ) { - global $wgHideInterlanguageLinks, $wgOut, $wgContLang; - if ( $wgHideInterlanguageLinks ) return ''; - - $links = $wgOut->getLanguageLinks(); - $s = ''; - if ( count( $links ) ) { - foreach( $links as $l ) { - $tmp = explode( ':', $l, 2 ); - $nt = Title::newFromText( $l ); - $s .= strtr( $inner, - array( - '$class' => htmlspecialchars( 'interwiki-' . $tmp[0] ), - '$href' => htmlspecialchars( $nt->getFullURL() ), - '$text' => ($wgContLang->getLanguageName( $nt->getInterwiki() ) != ''? - $wgContLang->getLanguageName( $nt->getInterwiki() ) : $l ), - ) - ); - } - $s = str_replace( '$body', $s, $outer ); - } - return cbt_value( $s, 'dynamic' ); - } - - function poweredbyico() { return $this->getPoweredBy(); } - function copyrightico() { return $this->getCopyrightIcon(); } - - function lastmod() { - global $wgMaxCredits; - if ( $wgMaxCredits ) return ''; - - if ( !isset( $this->mLastmod ) ) { - if ( $this->isCurrentArticleView() ) { - $this->mLastmod = $this->lastModified(); - } else { - $this->mLastmod = ''; - } - } - return cbt_value( $this->mLastmod, 'dynamic' ); - } - - function viewcount() { - global $wgDisableCounters; - if ( $wgDisableCounters ) return ''; - - global $wgLang, $wgArticle; - if ( is_object( $wgArticle ) ) { - $viewcount = $wgLang->formatNum( $wgArticle->getCount() ); - if ( $viewcount ) { - $viewcount = wfMsg( "viewcount", $viewcount ); - } else { - $viewcount = ''; - } - } else { - $viewcount = ''; - } - return cbt_value( $viewcount, 'dynamic' ); - } - - function numberofwatchingusers() { - global $wgPageShowWatchingUsers; - if ( !$wgPageShowWatchingUsers ) return ''; - - $dbr = wfGetDB( DB_SLAVE ); - extract( $dbr->tableNames( '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'); - $row = $dbr->fetchObject( $res ); - $num = $row->n; - if ($num > 0) { - $s = wfMsg('number_of_watching_users_pageview', $num); - } else { - $s = ''; - } - return cbt_value( $s, 'dynamic' ); - } - - function credits() { - global $wgMaxCredits; - if ( !$wgMaxCredits ) return ''; - - if ( $this->isCurrentArticleView() ) { - require_once("Credits.php"); - global $wgArticle, $wgShowCreditsIfMax; - $credits = getCredits($wgArticle, $wgMaxCredits, $wgShowCreditsIfMax); - } else { - $credits = ''; - } - return cbt_value( $credits, 'view dynamic' ); - } - - function normalcopyright() { - return $this->getCopyright( 'normal' ); - } - - function historycopyright() { - return $this->getCopyright( 'history' ); - } - - function is_currentview() { - global $wgRequest; - return cbt_value( (string)$this->isCurrentArticleView(), 'view' ); - } - - function usehistorycopyright() { - global $wgRequest; - if ( wfMsgForContent( 'history_copyright' ) == '-' ) return ''; - - $oldid = $this->getOldId(); - $diff = $this->getDiff(); - $use = (string)(!is_null( $oldid ) && is_null( $diff )); - return cbt_value( $use, 'nonview dynamic' ); - } - - function privacy() { - return cbt_value( $this->privacyLink(), 'lang' ); - } - function about() { - return cbt_value( $this->aboutLink(), 'lang' ); - } - function disclaimer() { - return cbt_value( $this->disclaimerLink(), 'lang' ); - } - function tagline() { - # A reference to this tag existed in the old MonoBook.php, but the - # template data wasn't set anywhere - return ''; - } - function reporttime() { - return cbt_value( $this->mOut->reportTime(), 'dynamic' ); - } - - function msg( $name ) { - return cbt_value( wfMsg( $name ), 'lang' ); - } - - function fallbackmsg( $name, $fallback ) { - $text = wfMsg( $name ); - if ( wfEmptyMsg( $name, $text ) ) { - $text = $fallback; - } - return cbt_value( $text, 'lang' ); - } - - /****************************************************** - * Utility functions * - ******************************************************/ - - /** Return true if this request is a valid, secure CSS preview */ - function isCssPreview() { - if ( !isset( $this->mCssPreview ) ) { - global $wgRequest, $wgAllowUserCss, $wgUser; - $this->mCssPreview = - $wgAllowUserCss && - $wgUser->isLoggedIn() && - $this->mTitle->isCssSubpage() && - $this->userCanPreview( $this->getAction() ); - } - return $this->mCssPreview; - } - - /** Return true if this request is a valid, secure JS preview */ - function isJsPreview() { - if ( !isset( $this->mJsPreview ) ) { - global $wgRequest, $wgAllowUserJs, $wgUser; - $this->mJsPreview = - $wgAllowUserJs && - $wgUser->isLoggedIn() && - $this->mTitle->isJsSubpage() && - $this->userCanPreview( $this->getAction() ); - } - return $this->mJsPreview; - } - - /** Get the title of the $wgUser's user page */ - function getUserPageTitle() { - if ( !isset( $this->mUserPageTitle ) ) { - global $wgUser; - $this->mUserPageTitle = $wgUser->getUserPage(); - } - return $this->mUserPageTitle; - } - - /** Get the text of the user page title */ - function getUserPageText() { - if ( !isset( $this->mUserPageText ) ) { - $userPage = $this->getUserPageTitle(); - $this->mUserPageText = $userPage->getPrefixedText(); - } - return $this->mUserPageText; - } - - /** Make an HTML element for a stylesheet link */ - function makeStylesheetLink( $url ) { - return '"; - } - - /** Make an XHTML element for inline CSS */ - function makeStylesheetCdata( $style ) { - return ""; - } - - /** Get the edit URL for this page */ - function getEditUrl() { - if ( !isset( $this->mEditUrl ) ) { - $this->mEditUrl = $this->mTitle->getLocalUrl( $this->editUrlOptions() ); - } - return $this->mEditUrl; - } - - /** Get the prefixed DB key for this page */ - function getThisPDBK() { - if ( !isset( $this->mThisPDBK ) ) { - $this->mThisPDBK = $this->mTitle->getPrefixedDbKey(); - } - return $this->mThisPDBK; - } - - function getThisTitleUrlForm() { - if ( !isset( $this->mThisTitleUrlForm ) ) { - $this->mThisTitleUrlForm = $this->mTitle->getPrefixedURL(); - } - return $this->mThisTitleUrlForm; - } - - /** - * If the current page is a user page, get the user's ID and IP. Otherwise return array(0,false) - */ - function getUserPageIdIp() { - if ( !isset( $this->mUserPageId ) ) { - if( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { - $this->mUserPageId = User::idFromName($this->mTitle->getText()); - $this->mUserPageIp = User::isIP($this->mTitle->getText()); - } else { - $this->mUserPageId = 0; - $this->mUserPageIp = false; - } - } - return array( $this->mUserPageId, $this->mUserPageIp ); - } - - /** - * Returns a permalink URL, or false if the current page is already a - * permalink, or blank if a permalink shouldn't be displayed - */ - function getPermalink() { - if ( !isset( $this->mPermalink ) ) { - global $wgRequest, $wgArticle; - $action = $this->getAction(); - $oldid = $this->getOldId(); - $url = ''; - if( $this->mTitle->getNamespace() !== NS_SPECIAL - && $this->mTitle->getArticleId() != 0 - && ($action == '' || $action == 'view' || $action == 'purge' ) ) - { - if ( !$oldid ) { - $revid = $wgArticle->getLatest(); - $url = $this->mTitle->getLocalURL( "oldid=$revid" ); - } else { - $url = false; - } - } else { - $url = ''; - } - } - return $url; - } - - /** - * Returns true if the current page is an article, not a special page, - * and we are viewing a revision, not a diff - */ - function isArticleView() { - global $wgOut, $wgArticle, $wgRequest; - if ( !isset( $this->mIsArticleView ) ) { - $oldid = $this->getOldId(); - $diff = $this->getDiff(); - $this->mIsArticleView = $wgOut->isArticle() and - (!is_null( $oldid ) or is_null( $diff )) and 0 != $wgArticle->getID(); - } - return $this->mIsArticleView; - } - - function isCurrentArticleView() { - if ( !isset( $this->mIsCurrentArticleView ) ) { - global $wgOut, $wgArticle, $wgRequest; - $oldid = $this->getOldId(); - $this->mIsCurrentArticleView = $wgOut->isArticle() && is_null( $oldid ) && 0 != $wgArticle->getID(); - } - return $this->mIsCurrentArticleView; - } - - - /** - * Return true if the current page is editable; if edit section on right - * click should be enabled. - */ - function isEditable() { - global $wgRequest; - $action = $this->getAction(); - return ($this->mTitle->getNamespace() != NS_SPECIAL and !($action == 'edit' or $action == 'submit')); - } - - /** Return true if the user is logged in */ - function isLoggedIn() { - global $wgUser; - return $wgUser->isLoggedIn(); - } - - /** Get the local URL of the current page */ - function getPageUrl() { - if ( !isset( $this->mPageUrl ) ) { - $this->mPageUrl = $this->mTitle->getLocalURL(); - } - return $this->mPageUrl; - } - - /** Make a link to a title using a template */ - function makeTemplateLink( $template, $key, $title, $text ) { - $url = $title->getLocalUrl(); - return strtr( $template, - array( - '$key' => $key, - '$classactive' => ($url == $this->getPageUrl()) ? 'class="active"' : '', - '$class' => $title->getArticleID() == 0 ? 'class="new"' : '', - '$href' => htmlspecialchars( $url ), - '$text' => $text - ) ); - } - - /** Make a link to a URL using a template */ - function makeTemplateLinkUrl( $template, $key, $url, $text ) { - return strtr( $template, - array( - '$key' => $key, - '$classactive' => ($url == $this->getPageUrl()) ? 'class="active"' : '', - '$class' => '', - '$href' => htmlspecialchars( $url ), - '$text' => $text - ) ); - } - - /** Make a link to a special page using a template */ - function makeSpecialTemplateLink( $template, $key, $specialName, $text, $query = '' ) { - $url = self::makeSpecialUrl( $specialName, $query ); - // Ignore the query when comparing - $active = ($this->mTitle->getNamespace() == NS_SPECIAL && $this->mTitle->getDBkey() == $specialName); - return strtr( $template, - array( - '$key' => $key, - '$classactive' => $active ? 'class="active"' : '', - '$class' => '', - '$href' => htmlspecialchars( $url ), - '$text' => $text - ) ); - } - - function loadRequestValues() { - global $wgRequest; - $this->mAction = $wgRequest->getText( 'action' ); - $this->mOldId = $wgRequest->getVal( 'oldid' ); - $this->mDiff = $wgRequest->getVal( 'diff' ); - $this->mSection = $wgRequest->getVal( 'section' ); - $this->mSearch = $wgRequest->getVal( 'search' ); - $this->mRequestValuesLoaded = true; - } - - - - /** Get the action parameter of the request */ - function getAction() { - if ( !isset( $this->mRequestValuesLoaded ) ) { - $this->loadRequestValues(); - } - return $this->mAction; - } - - /** Get the oldid parameter */ - function getOldId() { - if ( !isset( $this->mRequestValuesLoaded ) ) { - $this->loadRequestValues(); - } - return $this->mOldId; - } - - /** Get the diff parameter */ - function getDiff() { - if ( !isset( $this->mRequestValuesLoaded ) ) { - $this->loadRequestValues(); - } - return $this->mDiff; - } - - function getSection() { - if ( !isset( $this->mRequestValuesLoaded ) ) { - $this->loadRequestValues(); - } - return $this->mSection; - } - - function getSearch() { - if ( !isset( $this->mRequestValuesLoaded ) ) { - $this->loadRequestValues(); - } - return $this->mSearch; - } - - /** Make a special page URL of the form [[Special:Somepage/{title_urlform}]] */ - function makeSpecialParamUrl( $name, $query = '', $param = '{title_urlform}' ) { - // Abuse makeTitle's lax validity checking to slip a control character into the URL - $title = Title::makeTitle( NS_SPECIAL, "$name/\x1a" ); - $url = cbt_escape( $title->getLocalURL( $query ) ); - // Now replace it with the parameter - return str_replace( '%1A', $param, $url ); - } - - function getSubjectPage() { - if ( !isset( $this->mSubjectPage ) ) { - $this->mSubjectPage = $this->mTitle->getSubjectPage(); - } - return $this->mSubjectPage; - } - - function getTalkPage() { - if ( !isset( $this->mTalkPage ) ) { - $this->mTalkPage = $this->mTitle->getTalkPage(); - } - return $this->mTalkPage; - } -} - diff --git a/skins/modern/discussionitem_icon.gif b/skins/modern/discussionitem_icon.gif index baec471a..e3ca6d9e 100644 Binary files a/skins/modern/discussionitem_icon.gif and b/skins/modern/discussionitem_icon.gif differ diff --git a/skins/modern/file_icon.gif b/skins/modern/file_icon.gif index 847f6485..69dbeaf7 100644 Binary files a/skins/modern/file_icon.gif and b/skins/modern/file_icon.gif differ diff --git a/skins/modern/link_icon.gif b/skins/modern/link_icon.gif index 815ccb1b..168c1a2f 100644 Binary files a/skins/modern/link_icon.gif and b/skins/modern/link_icon.gif differ diff --git a/skins/modern/lock_icon.gif b/skins/modern/lock_icon.gif index 8a87e283..82844033 100644 Binary files a/skins/modern/lock_icon.gif and b/skins/modern/lock_icon.gif differ diff --git a/skins/modern/mail_icon.gif b/skins/modern/mail_icon.gif index 50a87a9a..cf5680d9 100644 Binary files a/skins/modern/mail_icon.gif and b/skins/modern/mail_icon.gif differ diff --git a/skins/modern/main.css b/skins/modern/main.css index 80ef008c..a25b6b59 100644 --- a/skins/modern/main.css +++ b/skins/modern/main.css @@ -153,7 +153,7 @@ textarea { } #searchInput { - display: block; + width: 85%; margin-left: auto; margin-right: auto; } @@ -368,7 +368,7 @@ h1, h2 { display: none; } -.prefsectiontip { +.htmlform-tip { font-size: x-small; padding: .2em 2em; color: #666; @@ -396,6 +396,10 @@ h1, h2 { color: white; } +#mw-pref-clear { + clear: both; +} + #mw_content a.external, #mw_content a[href ^="gopher://"] { background: url(external.png) center right no-repeat; @@ -531,12 +535,6 @@ img.thumbborder { .hiddenStructure { display: none; } - -#mw_content .plainlinks a { - background: none !important; - padding: 0 !important; -} - .mw-warning { border: 1px solid #aaa; background-color: #f9f9f9; @@ -607,15 +605,10 @@ img.thumbborder { .catlinks { border: solid 1px #bbbbbb; background-color: #f0f0f0; - padding: 0.5em 0.5em 0.5em 0.5em; + padding: 0.1em 0.3em 0.1em 0.3em; margin: 0 0 0 0; } -.catlinks { - margin: 0 0 0 0; - padding: 0 0 0 0; -} - #mw_header h1, #p-personal, #p-cactions { @@ -644,21 +637,6 @@ img.thumbborder { display: none; } -.not-patrolled { - background-color: #ffa; -} -div.patrollink { - font-size: 75%; - text-align: right; -} -span.newpage, span.minor, span.bot { - font-weight: bold; -} -span.unpatrolled { - font-weight: bold; - color: red; -} - .sharedUploadNotice { font-style: italic; } @@ -668,55 +646,6 @@ span.updatedmarker { background-color: #0f0; } -table.gallery { - border: 1px solid #ccc; - margin: 2px; - padding: 2px; - background-color: white; -} - -table.gallery tr { - vertical-align: top; -} - -table.gallery td { - vertical-align: top; - background-color: #f9f9f9; - border: solid 2px white; -} -/* Keep this temporarily so that cached pages will display right */ -table.gallery td.galleryheader { - text-align: center; - font-weight: bold; -} -table.gallery caption { - font-weight: bold; -} - -div.gallerybox { - margin: 2px; -} - -div.gallerybox div.thumb { - text-align: center; - border: 1px solid #ccc; - margin: 2px; -} - -div.gallerytext { - overflow: hidden; - font-size: 94%; - padding: 2px 4px; -} - -span.comment { - font-style: italic; -} - -span.changedby { - font-size: 95%; -} - .previewnote { text-indent: 3em; color: #c00; @@ -749,69 +678,10 @@ span.changedby { text-indent: -2em; } -/* Classes for EXIF data display */ -table.mw_metadata { - font-size: 0.8em; - margin-left: 0.5em; - margin-bottom: 0.5em; - width: 300px; -} - -table.mw_metadata caption { - font-weight: bold; -} - -table.mw_metadata th { - font-weight: normal; -} - -table.mw_metadata td { - padding: 0.1em; -} - -table.mw_metadata { - border: none; - border-collapse: collapse; -} - -table.mw_metadata td, table.mw_metadata th { - text-align: center; - border: 1px solid #aaaaaa; - padding-left: 0.1em; - padding-right: 0.1em; -} - -table.mw_metadata th { - background-color: #f9f9f9; -} - -table.mw_metadata td { - background-color: #fcfcfc; -} - table.collapsed tr.collapsable { display: none; } - -/* filetoc */ -ul#filetoc { - text-align: center; - border: 1px solid #aaaaaa; - background-color: #f9f9f9; - padding: 5px; - font-size: 95%; - margin-bottom: 0.5em; - margin-left: 0; - margin-right: 0; -} - -#filetoc li { - display: inline; - list-style-type: none; - padding-right: 2em; -} - input#wpSummary { width: 80%; } @@ -849,32 +719,6 @@ p.revision_saved { font-weight:bold; } -#mw_trackbacks { - border: solid 1px #bbbbff; - background-color: #eeeeff; - padding: 0.2em; -} - - -/* Allmessages table */ - -#allmessagestable th { - background-color: #b2b2ff; -} - -#allmessagestable tr.orig { - background-color: #ffe2e2; -} - -#allmessagestable tr.new { - background-color: #e2ffe2; -} - -#allmessagestable tr.def { - background-color: #f0f0ff; -} - - /* noarticletext */ div.noarticletext { border: 1px solid #ccc; @@ -934,29 +778,6 @@ table.multipageimage td { text-align: center; } -/** Special:Version */ - -table#sv-ext, table#sv-hooks, table#sv-software { - margin: 1em; - padding:0em; -} - -#sv-ext td, #sv-hooks td, #sv-software td, -#sv-ext th, #sv-hooks th, #sv-software th { - border: 1px solid #A0A0A0; - padding: 0 0.15em 0 0.15em; -} -#sv-ext th, #sv-hooks th, #sv-software th { - background-color: #F0F0F0; - color: black; - padding: 0 0.15em 0 0.15em; -} -tr.sv-space{ - height: 0.8em; - border:none; -} -tr.sv-space td { display: none; } - /* Table pager (e.g. Special:Imagelist) - remove underlines from the navigation link diff --git a/skins/modern/rtl.css b/skins/modern/rtl.css index d2dcacec..72210480 100644 --- a/skins/modern/rtl.css +++ b/skins/modern/rtl.css @@ -79,7 +79,6 @@ input#wpSave, input#wpDiff { } #userlogin { - float: right; margin: 0 0 1em 3em; } /* Convenience links to edit block, delete and protect reasons */ @@ -152,6 +151,11 @@ html > body div#mw_contentholder ul#filetoc { display: block; } +/* Special:Allpages styling */ +td.mw-allpages-nav, p.mw-allpages-nav, td.mw-allpages-alphaindexline { + text-align: left; +} + /* Special:Prefixindex styling */ td#mw-prefixindex-nav-form { text-align: left; diff --git a/skins/monobook/IEMacFixes.css b/skins/monobook/IEMacFixes.css deleted file mode 100644 index f1b05719..00000000 --- a/skins/monobook/IEMacFixes.css +++ /dev/null @@ -1,44 +0,0 @@ -/* IE/Mac only fix stylesheet, imported from main.css */ -#portal-column-content { - margin: 0 0 4.8em 0; - float: none; -} -#portal-column-content #content { - z-index: 0; -} -#portal-column-one { - position: absolute; - top: 0; - left: 0; - z-index: 3; -} -#portal-footer { - margin-left: 12em; -} -/* -#portlet-contentViews { - top: 0.6em !important; - left: 14.5em !important; -} -*/ -#portlet-contentViews li, -#portlet-contentViews .selected { - border: none !important; -} -#portlet-contentViews li a { - border: 1px solid #aaaaaa; - border-bottom: none; -} -#portlet-contentViews li.selected a { - border: 1px solid #fabd23; - border-bottom: none; -} -/* no background images */ -li#personaltools-userpage, -li#personaltools-login/* */ { - background: none; - padding-left: none; -} -#mactest { - color: green; -} diff --git a/skins/monobook/discussionitem_icon.gif b/skins/monobook/discussionitem_icon.gif index baec471a..e3ca6d9e 100644 Binary files a/skins/monobook/discussionitem_icon.gif and b/skins/monobook/discussionitem_icon.gif differ diff --git a/skins/monobook/file_icon.gif b/skins/monobook/file_icon.gif index 847f6485..69dbeaf7 100644 Binary files a/skins/monobook/file_icon.gif and b/skins/monobook/file_icon.gif differ diff --git a/skins/monobook/link_icon.gif b/skins/monobook/link_icon.gif index 815ccb1b..168c1a2f 100644 Binary files a/skins/monobook/link_icon.gif and b/skins/monobook/link_icon.gif differ diff --git a/skins/monobook/lock_icon.gif b/skins/monobook/lock_icon.gif index 8a87e283..f71cd9b8 100644 Binary files a/skins/monobook/lock_icon.gif and b/skins/monobook/lock_icon.gif differ diff --git a/skins/monobook/magnify-clip.png b/skins/monobook/magnify-clip.png index 992aa2e3..ffd7637f 100644 Binary files a/skins/monobook/magnify-clip.png and b/skins/monobook/magnify-clip.png differ diff --git a/skins/monobook/mail_icon.gif b/skins/monobook/mail_icon.gif index 50a87a9a..cf5680d9 100644 Binary files a/skins/monobook/mail_icon.gif and b/skins/monobook/mail_icon.gif differ diff --git a/skins/monobook/main.css b/skins/monobook/main.css index d6a67c1e..727355bb 100644 --- a/skins/monobook/main.css +++ b/skins/monobook/main.css @@ -54,9 +54,6 @@ body { margin: 0; padding: 0; } -.visualClear { - clear: both; -} /* general styles */ @@ -281,43 +278,12 @@ span.subpages { #siteNotice { text-align: center; font-size: 95%; - padding: 0 .9em; + padding: 0 0.9em; } #siteNotice p { margin: 0; padding: 0; } -.success { - color: green; - font-size: larger; -} -.error { - color: red; - font-size: larger; -} -.errorbox, .successbox { - font-size: larger; - border: 2px solid; - padding: .5em 1em; - float: left; - margin-bottom: 2em; - color: #000; -} -.errorbox { - border-color: red; - background-color: #fff2f2; -} -.successbox { - border-color: green; - background-color: #dfd; -} -.errorbox h2, .successbox h2 { - font-size: 1em; - font-weight: bold; - display: inline; - margin: 0 .5em 0 0; - border: none; -} .catlinks { border: 1px solid #aaa; @@ -525,35 +491,34 @@ table.rimage { ** this is css3, the validator doesn't like it when validating as css2 */ #bodyContent a.external, -#bodyContent a[href ^="gopher://"] { +#bodyContent a.external[href ^="gopher://"] { background: url(external.png) center right no-repeat; padding: 0 13px; } .rtl #bodyContent a.external, -.rtl #bodyContent a[href ^="gopher://"] { +.rtl #bodyContent a.external[href ^="gopher://"] { background-image: url(external-rtl.png); } -#bodyContent a[href ^="https://"], +#bodyContent a.external[href ^="https://"], .link-https { background: url(lock_icon.gif) center right no-repeat; padding: 0 16px; } -#bodyContent a[href ^="mailto:"], +#bodyContent a.external[href ^="mailto:"], .link-mailto { background: url(mail_icon.gif) center right no-repeat; padding: 0 18px; } -#bodyContent a[href ^="news://"] { +#bodyContent a.external[href ^="news://"] { background: url(news_icon.png) center right no-repeat; padding: 0 18px; } -#bodyContent a[href ^="ftp://"], +#bodyContent a.external[href ^="ftp://"], .link-ftp { background: url(file_icon.gif) center right no-repeat; padding: 0 18px; } -#bodyContent a[href ^="irc://"], -#bodyContent a.extiw[href ^="irc://"], +#bodyContent a.external[href ^="irc://"], .link-irc { background: url(discussionitem_icon.gif) center right no-repeat; padding: 0 18px; @@ -604,18 +569,10 @@ table.rimage { #bodyContent a.extiw, #bodyContent a.extiw:active { color: #36b; - background: none; - padding: 0; } #bodyContent a.external { color: #36b; } -/* this can be used in the content area to switch off -special external link styling */ -#bodyContent .plainlinks a { - background: none !important; - padding: 0 !important; -} /* ** Structural Elements */ @@ -920,6 +877,14 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { z-index: 3; } +/* Override text-transform on languages where capitalization is significant */ +.capitalize-all-nouns .portlet h5, +.capitalize-all-nouns .portlet h6, +.capitalize-all-nouns #p-personal ul, +.capitalize-all-nouns #p-cactions ul li a { + text-transform: none; +} + /* TODO: #t-iscite is only used by the Cite extension, come up with some * system which allows extensions to add to this file on the fly */ @@ -956,6 +921,13 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { height: 1%; } +.mw-htmlform-submit { + font-weight: bold; + padding-left: .3em; + padding-right: .3em; + margin-right: 2em; +} + /* js pref toc */ #preftoc { margin: 0; @@ -1005,10 +977,6 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { cursor: default; text-decoration: none; } -#prefcontrol { - padding-top: 2em; - clear: both; -} #preferences { margin: 0; border: 1px solid #aaa; @@ -1021,11 +989,7 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { padding: 0; margin: 0; } -.prefsection fieldset { - border: 1px solid #aaa; - float: left; - margin-right: 2em; -} + .prefsection legend { font-weight: bold; } @@ -1035,16 +999,11 @@ li#ca-watch, li#ca-unwatch, li#ca-varlang-0, li#ca-print { .mainLegend { display: none; } -div.prefsectiontip { +td.htmlform-tip { font-size: x-small; padding: .2em 2em; color: #666; } -.btnSavePrefs { - font-weight: bold; - padding-left: .3em; - padding-right: .3em; -} .preferences-login { clear: both; @@ -1162,20 +1121,9 @@ div#userloginForm .captcha { display: none; } -.not-patrolled { - background-color: #ffa; -} + div.patrollink { clear: both; - font-size: 75%; - text-align: right; -} -span.newpage, span.minor, span.bot { - font-weight: bold; -} -span.unpatrolled { - font-weight: bold; - color: red; } .sharedUploadNotice { @@ -1187,66 +1135,14 @@ span.updatedmarker { background-color: #0f0; } -table.gallery { - border: 1px solid #ccc; - margin: 2px; - padding: 2px; - background-color: white; -} - -table.gallery tr { - vertical-align: top; -} - -table.gallery td { - vertical-align: top; - background-color: #f9f9f9; - border: solid 2px white; -} -/* Keep this temporarily so that cached pages will display right */ -table.gallery td.galleryheader { - text-align: center; - font-weight: bold; -} -table.gallery caption { - font-weight: bold; -} - -div.gallerybox { - margin: 2px; -} - -div.gallerybox div.thumb { - text-align: center; - border: 1px solid #ccc; - margin: 2px; -} - -div.gallerytext { - overflow: hidden; - font-size: 94%; - padding: 2px 4px; -} - -span.comment { - font-style: italic; -} - -span.changedby { - font-size: 95%; -} - .previewnote { - text-indent: 3em; color: #c00; - border-bottom: 1px solid #aaa; - padding-bottom: 1em; margin-bottom: 1em; } .previewnote p { - margin: 0; - padding: 0; + text-indent: 3em; + margin: 0.8em 0; } .editExternally { @@ -1268,69 +1164,6 @@ span.changedby { text-indent: -2em; } -/* Classes for EXIF data display */ -table.mw_metadata { - font-size: 0.8em; - margin-left: 0.5em; - margin-bottom: 0.5em; - width: 300px; -} - -table.mw_metadata caption { - font-weight: bold; -} - -table.mw_metadata th { - font-weight: normal; -} - -table.mw_metadata td { - padding: 0.1em; -} - -table.mw_metadata { - border: none; - border-collapse: collapse; -} - -table.mw_metadata td, table.mw_metadata th { - text-align: center; - border: 1px solid #aaaaaa; - padding-left: 0.1em; - padding-right: 0.1em; -} - -table.mw_metadata th { - background-color: #f9f9f9; -} - -table.mw_metadata td { - background-color: #fcfcfc; -} - -table.collapsed tr.collapsable { - display: none; -} - - -/* filetoc */ -ul#filetoc { - text-align: center; - border: 1px solid #aaaaaa; - background-color: #f9f9f9; - padding: 5px; - font-size: 95%; - margin-bottom: 0.5em; - margin-left: 0; - margin-right: 0; -} - -#filetoc li { - display: inline; - list-style-type: none; - padding-right: 2em; -} - input#wpSummary { width: 80%; } @@ -1368,32 +1201,6 @@ p.revision_saved { font-weight:bold; } -#mw_trackbacks { - border: solid 1px #bbbbff; - background-color: #eeeeff; - padding: 0.2em; -} - - -/* Allmessages table */ - -#allmessagestable th { - background-color: #b2b2ff; -} - -#allmessagestable tr.orig { - background-color: #ffe2e2; -} - -#allmessagestable tr.new { - background-color: #e2ffe2; -} - -#allmessagestable tr.def { - background-color: #f0f0ff; -} - - /* noarticletext */ div.noarticletext { border: 1px solid #ccc; @@ -1457,53 +1264,6 @@ table.multipageimage td { text-align: center; } -/** Special:Version */ - -table#sv-ext, table#sv-hooks, table#sv-software { - margin: 1em; - padding:0em; -} - -#sv-ext td, #sv-hooks td, #sv-software td, -#sv-ext th, #sv-hooks th, #sv-software th { - border: 1px solid #A0A0A0; - padding: 0 0.15em 0 0.15em; -} -#sv-ext th, #sv-hooks th, #sv-software th { - background-color: #F0F0F0; - color: black; - padding: 0 0.15em 0 0.15em; -} -tr.sv-space{ - height: 0.8em; - border:none; -} -tr.sv-space td { display: none; } - -/* - Table pager (e.g. Special:Imagelist) - - remove underlines from the navigation link - - collapse borders - - set the borders to outsets (similar to Special:Allmessages) - - remove line wrapping for all td and th, set background color - - restore line wrapping for the last two table cells (description and size) -*/ -.TablePager { min-width: 80%; } -.TablePager_nav a { text-decoration: none; } -.TablePager { border-collapse: collapse; } -.TablePager, .TablePager td, .TablePager th { - border: 1px solid #aaaaaa; - padding: 0 0.15em 0 0.15em; -} -.TablePager th { background-color: #eeeeff } -.TablePager td { background-color: #ffffff } -.TablePager tr:hover td { background-color: #eeeeff } - -.imagelist td, .imagelist th { white-space: nowrap } -.imagelist .TablePager_col_links { background-color: #eeeeff } -.imagelist .TablePager_col_img_description { white-space: normal } -.imagelist th.TablePager_sort { background-color: #ccccff } - .templatesUsed { margin-top: 1.5em; } .mw-summary-preview { @@ -1537,24 +1297,12 @@ div.mw-lag-warn-high { font-size: 90%; } -/** Special:Search stuff */ -div#mw-search-interwiki-caption { - text-align: center; - font-weight: bold; - font-size: 95%; -} - -.mw-search-interwiki-project { - font-size: 97%; - text-align: left; - padding-left: 0.2em; - padding-right: 0.15em; - padding-bottom: 0.2em; - padding-top: 0.15em; - background: #cae8ff; -} - /* God-damned hack for the crappy layout */ .os-suggest { font-size: 127%; } + +/* Sometimes people don't want personal tools to be lowercase! */ +.no-text-transform { + text-transform: none; +} diff --git a/skins/monobook/rtl.css b/skins/monobook/rtl.css index 9b8e4f44..b9bf43c0 100644 --- a/skins/monobook/rtl.css +++ b/skins/monobook/rtl.css @@ -211,7 +211,6 @@ input#wpSave, input#wpDiff { } #userlogin { - float: right; margin: 0 0 1em 3em; } /* Convenience links to edit block, delete and protect reasons */ @@ -228,20 +227,25 @@ table.filehistory th { text-align: right; } +/* Special:Allpages styling */ +td.mw-allpages-nav, p.mw-allpages-nav, td.mw-allpages-alphaindexline { + text-align: left; +} + +/* Special:Prefixindex styling */ +td#mw-prefixindex-nav-form { + text-align: left; +} + /** * Lists: * The following lines don't have a visible effect on non-Gecko browsers * They fix a problem ith Gecko browsers rendering lists to the right of * left-floated objects in an RTL layout. */ -html > body div#bodyContent ul { +html > body div#article ul { display: table; } html > body div#bodyContent ul#filetoc { display: block; } -/* Special:Prefixindex styling */ -td#mw-prefixindex-nav-form { - text-align: left; -} - diff --git a/skins/monobook/user.gif b/skins/monobook/user.gif index c9c9ab96..34b4839d 100644 Binary files a/skins/monobook/user.gif and b/skins/monobook/user.gif differ diff --git a/skins/monobook/video.png b/skins/monobook/video.png index 38103dac..fadc4c9b 100644 Binary files a/skins/monobook/video.png and b/skins/monobook/video.png differ diff --git a/skins/monobook/wiki.png b/skins/monobook/wiki.png index 69fce988..44389389 100644 Binary files a/skins/monobook/wiki.png and b/skins/monobook/wiki.png differ diff --git a/skins/simple/discussionitem_icon.gif b/skins/simple/discussionitem_icon.gif index baec471a..e3ca6d9e 100644 Binary files a/skins/simple/discussionitem_icon.gif and b/skins/simple/discussionitem_icon.gif differ diff --git a/skins/simple/file_icon.gif b/skins/simple/file_icon.gif index 847f6485..69dbeaf7 100644 Binary files a/skins/simple/file_icon.gif and b/skins/simple/file_icon.gif differ diff --git a/skins/simple/link_icon.gif b/skins/simple/link_icon.gif index 815ccb1b..168c1a2f 100644 Binary files a/skins/simple/link_icon.gif and b/skins/simple/link_icon.gif differ diff --git a/skins/simple/lock_icon.gif b/skins/simple/lock_icon.gif index 8a87e283..82844033 100644 Binary files a/skins/simple/lock_icon.gif and b/skins/simple/lock_icon.gif differ diff --git a/skins/simple/mail_icon.gif b/skins/simple/mail_icon.gif index 50a87a9a..cf5680d9 100644 Binary files a/skins/simple/mail_icon.gif and b/skins/simple/mail_icon.gif differ diff --git a/skins/simple/main.css b/skins/simple/main.css index 6ba47e0d..525c1473 100644 --- a/skins/simple/main.css +++ b/skins/simple/main.css @@ -281,13 +281,13 @@ div.thumb { margin-bottom: 0.5em; width: auto; } -div.thumb div { +div.thumbinner { padding: 3px !important; text-align: center; overflow: hidden; } -div.thumb div div.thumbcaption { +html .thumbcaption { border: none; text-align: left; line-height: 1.4; @@ -365,12 +365,7 @@ div.printfooter { #preftoc a:active { display: block; } -#prefcontrol { - clear: both; - float: left; - margin-top: 1em; -} -div.prefsectiontip { +div.htmlform-tip { font-size: 95%; margin-top: 1em; } diff --git a/skins/simple/rtl.css b/skins/simple/rtl.css index 53b3dc0c..a5a2b59f 100644 --- a/skins/simple/rtl.css +++ b/skins/simple/rtl.css @@ -148,7 +148,6 @@ input#wpSave, input#wpDiff { } #userlogin { - float: right; margin: 0 0 1em 3em; } /* Convenience links to edit block, delete and protect reasons */ @@ -174,6 +173,11 @@ table.filehistory th { html > body div#bodyContent ul { display: table; } + +/* Special:Allpages styling */ +td.mw-allpages-nav, p.mw-allpages-nav, td.mw-allpages-alphaindexline { + text-align: left; + /* Special:Prefixindex styling */ td#mw-prefixindex-nav-form { text-align: left; diff --git a/skins/vector/Makefile b/skins/vector/Makefile new file mode 100644 index 00000000..74e36c45 --- /dev/null +++ b/skins/vector/Makefile @@ -0,0 +1,18 @@ +# +# Handy makefile to build the RTL variant with cssjanus +# + +all: main-rtl.css + +main-rtl.css: main-ltr.css cssjanus/cssjanus.py + python cssjanus/cssjanus.py --swap_ltr_rtl_in_url < main-ltr.css > main-rtl.css + +# SVN version is broken; checking in our own. +#cssjanus/cssjanus.py: +# svn co http://cssjanus.googlecode.com/svn/trunk cssjanus + +#distclean: clean +# rm -rf cssjanus + +clean: + rm -f main-rtl.css diff --git a/skins/vector/csshover.htc b/skins/vector/csshover.htc new file mode 100644 index 00000000..a88fa08d --- /dev/null +++ b/skins/vector/csshover.htc @@ -0,0 +1,262 @@ + + \ No newline at end of file diff --git a/skins/vector/cssjanus/COPYING b/skins/vector/cssjanus/COPYING new file mode 100644 index 00000000..3f2c8953 --- /dev/null +++ b/skins/vector/cssjanus/COPYING @@ -0,0 +1,13 @@ + Copyright 2008 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/skins/vector/cssjanus/LICENSE b/skins/vector/cssjanus/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/skins/vector/cssjanus/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/skins/vector/cssjanus/README b/skins/vector/cssjanus/README new file mode 100644 index 00000000..9b922156 --- /dev/null +++ b/skins/vector/cssjanus/README @@ -0,0 +1,91 @@ +=CSSJanus= + +_Flips CSS from LTR to an RTL orienation and vice-versa_ + +Author: `Lindsey Simon ` + +==Introduction== + +CSSJanus is CSS parser utility designed to aid the conversion of a website's +layout from left-to-right(LTR) to right-to-left(RTL). The script was born out of +a need to convert CSS for RTL languages when tables are not being used for layout (since tables will automatically reorder TD's in RTL). +CSSJanus will change most of the obvious CSS property names and their values as +well as some not-so-obvious ones (cursor, background-position %, etc...). +The script is designed to offer flexibility to account for cases when you do +not want to change certain rules which exist to account for bidirectional text +display bugs, as well as situations where you may or may not want to flip annotations inside of the background url string. +Note that you can disable CSSJanus from running on an entire class or any +rule within a class by prepending a /* @noflip */ comment before the rule(s) +you want CSSJanus to ignore. + +CSSJanus itself is not always enough to make a website that works in a LTR +language context work in a RTL language all the way, but it is a start. + +==Getting the code== + +View the trunk at: + + http://cssjanus.googlecode.com/svn/trunk/ + +Check out the latest development version anonymously with: + +{{{ + $ svn checkout http://cssjanus.googlecode.com/svn/trunk/ cssjanus +}}} + +==Using== + +Usage: + ./cssjanus.py < file.css > file-rtl.css +Flags: + --swap_left_right_in_url: Fixes "left"/"right" string within urls. + Ex: ./cssjanus.py --swap_left_right_in_url < file.css > file_rtl.css + --swap_ltr_rtl_in_url: Fixes "ltr"/"rtl" string within urls. + Ex: ./cssjanus.py --swap_ltr_rtl_in_url < file.css > file_rtl.css + +If you'd like to make use of the webapp version of cssjanus, you'll need to +download the Google App Engine SDK + http://code.google.com/appengine/downloads.html +and also drop a "django" directory into this directory, with the latest svn +from django. You should be good to go with that setup. Please let me know +otherwise. + +==Bugs, Patches== + +Patches and bug reports are welcome, just please keep the style +consistent with the original source. If you find a bug, please include a diff +of cssjanus_test.py with the bug included as a new unit test which fails. It +will make understanding and fixing the bug easier. + +==Todo== + +* Include some helpers for some typical bidi text solutions? +* Aural CSS (azimuth) swapping? + +==Contributors== + +Additional thanks to Mike Samuel for his work on csslex.py, Andy Perelson for +his help coding and reviewing, Stephen Zabel for his help with i18n and my sanity, +and to Eric Meyer for his thoughtful input. +Thanks to Junyu Wang for the Chinese translation. +Thanks to Masashi Kawashima for the Japanese translation. +Thanks to Taaryk Taar and Tariq Al-Omaireeni for an updated Arabic translation. +Thanks to Jens Meiert for the German translation. + +==License== + +{{{ + Copyright 2008 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the 'License'); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an 'AS IS' BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +}}} diff --git a/skins/vector/cssjanus/cssjanus.py b/skins/vector/cssjanus/cssjanus.py new file mode 100644 index 00000000..dd14bd58 --- /dev/null +++ b/skins/vector/cssjanus/cssjanus.py @@ -0,0 +1,574 @@ +#!/usr/bin/python +# +# Copyright 2008 Google Inc. All Rights Reserved. + +"""Converts a LeftToRight Cascading Style Sheet into a RightToLeft one. + + This is a utility script for replacing "left" oriented things in a CSS file + like float, padding, margin with "right" oriented values. + It also does the opposite. + The goal is to be able to conditionally serve one large, cat'd, compiled CSS + file appropriate for LeftToRight oriented languages and RightToLeft ones. + This utility will hopefully help your structural layout done in CSS in + terms of its RTL compatibility. It will not help with some of the more + complicated bidirectional text issues. +""" + +__author__ = 'elsigh@google.com (Lindsey Simon)' +__version__ = '0.1' + +import logging +import re +import sys +import getopt +import os + +import csslex + +logging.getLogger().setLevel(logging.INFO) + +# Global for the command line flags. +SWAP_LTR_RTL_IN_URL_DEFAULT = False +SWAP_LEFT_RIGHT_IN_URL_DEFAULT = False +FLAGS = {'swap_ltr_rtl_in_url': SWAP_LTR_RTL_IN_URL_DEFAULT, + 'swap_left_right_in_url': SWAP_LEFT_RIGHT_IN_URL_DEFAULT} + +# Generic token delimiter character. +TOKEN_DELIMITER = '~' + +# This is a temporary match token we use when swapping strings. +TMP_TOKEN = '%sTMP%s' % (TOKEN_DELIMITER, TOKEN_DELIMITER) + +# Token to be used for joining lines. +TOKEN_LINES = '%sJ%s' % (TOKEN_DELIMITER, TOKEN_DELIMITER) + +# Global constant text strings for CSS value matches. +LTR = 'ltr' +RTL = 'rtl' +LEFT = 'left' +RIGHT = 'right' + +# This is a lookbehind match to ensure that we don't replace instances +# of our string token (left, rtl, etc...) if there's a letter in front of it. +# Specifically, this prevents replacements like 'background: url(bright.png)'. +LOOKBEHIND_NOT_LETTER = r'(?)*?{)' % + (csslex.NMCHAR, TOKEN_LINES, csslex.SPACE)) + + +# These two lookaheads are to test whether or not we are within a +# background: url(HERE) situation. +# Ref: http://www.w3.org/TR/CSS21/syndata.html#uri +VALID_AFTER_URI_CHARS = r'[\'\"]?%s' % csslex.WHITESPACE +LOOKAHEAD_NOT_CLOSING_PAREN = r'(?!%s?%s\))' % (csslex.URL_CHARS, + VALID_AFTER_URI_CHARS) +LOOKAHEAD_FOR_CLOSING_PAREN = r'(?=%s?%s\))' % (csslex.URL_CHARS, + VALID_AFTER_URI_CHARS) + +# Compile a regex to swap left and right values in 4 part notations. +# We need to match negatives and decimal numeric values. +# ex. 'margin: .25em -2px 3px 0' becomes 'margin: .25em 0 3px -2px'. +POSSIBLY_NEGATIVE_QUANTITY = r'((?:-?%s)|(?:inherit|auto))' % csslex.QUANTITY +POSSIBLY_NEGATIVE_QUANTITY_SPACE = r'%s%s%s' % (POSSIBLY_NEGATIVE_QUANTITY, + csslex.SPACE, + csslex.WHITESPACE) +FOUR_NOTATION_QUANTITY_RE = re.compile(r'%s%s%s%s' % + (POSSIBLY_NEGATIVE_QUANTITY_SPACE, + POSSIBLY_NEGATIVE_QUANTITY_SPACE, + POSSIBLY_NEGATIVE_QUANTITY_SPACE, + POSSIBLY_NEGATIVE_QUANTITY), + re.I) +COLOR = r'(%s|%s)' % (csslex.NAME, csslex.HASH) +COLOR_SPACE = r'%s%s' % (COLOR, csslex.SPACE) +FOUR_NOTATION_COLOR_RE = re.compile(r'(-color%s:%s)%s%s%s(%s)' % + (csslex.WHITESPACE, + csslex.WHITESPACE, + COLOR_SPACE, + COLOR_SPACE, + COLOR_SPACE, + COLOR), + re.I) + +# Compile the cursor resize regexes +CURSOR_EAST_RE = re.compile(LOOKBEHIND_NOT_LETTER + '([ns]?)e-resize') +CURSOR_WEST_RE = re.compile(LOOKBEHIND_NOT_LETTER + '([ns]?)w-resize') + +# Matches the condition where we need to replace the horizontal component +# of a background-position value when expressed in horizontal percentage. +# Had to make two regexes because in the case of position-x there is only +# one quantity, and otherwise we don't want to match and change cases with only +# one quantity. +BG_HORIZONTAL_PERCENTAGE_RE = re.compile(r'background(-position)?(%s:%s)' + '([^%%]*?)(%s)%%' + '(%s(?:%s|%s))' % (csslex.WHITESPACE, + csslex.WHITESPACE, + csslex.NUM, + csslex.WHITESPACE, + csslex.QUANTITY, + csslex.IDENT)) + +BG_HORIZONTAL_PERCENTAGE_X_RE = re.compile(r'background-position-x(%s:%s)' + '(%s)%%' % (csslex.WHITESPACE, + csslex.WHITESPACE, + csslex.NUM)) + +# Matches the opening of a body selector. +BODY_SELECTOR = r'body%s{%s' % (csslex.WHITESPACE, csslex.WHITESPACE) + +# Matches anything up until the closing of a selector. +CHARS_WITHIN_SELECTOR = r'[^\}]*?' + +# Matches the direction property in a selector. +DIRECTION_RE = r'direction%s:%s' % (csslex.WHITESPACE, csslex.WHITESPACE) + +# These allow us to swap "ltr" with "rtl" and vice versa ONLY within the +# body selector and on the same line. +BODY_DIRECTION_LTR_RE = re.compile(r'(%s)(%s)(%s)(ltr)' % + (BODY_SELECTOR, CHARS_WITHIN_SELECTOR, + DIRECTION_RE), + re.I) +BODY_DIRECTION_RTL_RE = re.compile(r'(%s)(%s)(%s)(rtl)' % + (BODY_SELECTOR, CHARS_WITHIN_SELECTOR, + DIRECTION_RE), + re.I) + + +# Allows us to swap "direction:ltr" with "direction:rtl" and +# vice versa anywhere in a line. +DIRECTION_LTR_RE = re.compile(r'%s(ltr)' % DIRECTION_RE) +DIRECTION_RTL_RE = re.compile(r'%s(rtl)' % DIRECTION_RE) + +# We want to be able to switch left with right and vice versa anywhere +# we encounter left/right strings, EXCEPT inside the background:url(). The next +# two regexes are for that purpose. We have alternate IN_URL versions of the +# regexes compiled in case the user passes the flag that they do +# actually want to have left and right swapped inside of background:urls. +LEFT_RE = re.compile('%s(%s)%s%s' % (LOOKBEHIND_NOT_LETTER, + LEFT, + LOOKAHEAD_NOT_CLOSING_PAREN, + LOOKAHEAD_NOT_OPEN_BRACE), + re.I) +RIGHT_RE = re.compile('%s(%s)%s%s' % (LOOKBEHIND_NOT_LETTER, + RIGHT, + LOOKAHEAD_NOT_CLOSING_PAREN, + LOOKAHEAD_NOT_OPEN_BRACE), + re.I) +LEFT_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER, + LEFT, + LOOKAHEAD_FOR_CLOSING_PAREN), + re.I) +RIGHT_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER, + RIGHT, + LOOKAHEAD_FOR_CLOSING_PAREN), + re.I) +LTR_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER, + LTR, + LOOKAHEAD_FOR_CLOSING_PAREN), + re.I) +RTL_IN_URL_RE = re.compile('%s(%s)%s' % (LOOKBEHIND_NOT_LETTER, + RTL, + LOOKAHEAD_FOR_CLOSING_PAREN), + re.I) + +COMMENT_RE = re.compile('(%s)' % csslex.COMMENT, re.I) + +NOFLIP_TOKEN = r'\@noflip' +# The NOFLIP_TOKEN inside of a comment. For now, this requires that comments +# be in the input, which means users of a css compiler would have to run +# this script first if they want this functionality. +NOFLIP_ANNOTATION = r'/\*%s%s%s\*/' % (csslex.WHITESPACE, + NOFLIP_TOKEN, + csslex. WHITESPACE) + +# After a NOFLIP_ANNOTATION, and within a class selector, we want to be able +# to set aside a single rule not to be flipped. We can do this by matching +# our NOFLIP annotation and then using a lookahead to make sure there is not +# an opening brace before the match. +NOFLIP_SINGLE_RE = re.compile(r'(%s%s[^;}]+;?)' % (NOFLIP_ANNOTATION, + LOOKAHEAD_NOT_OPEN_BRACE), + re.I) + +# After a NOFLIP_ANNOTATION, we want to grab anything up until the next } which +# means the entire following class block. This will prevent all of its +# declarations from being flipped. +NOFLIP_CLASS_RE = re.compile(r'(%s%s})' % (NOFLIP_ANNOTATION, + CHARS_WITHIN_SELECTOR), + re.I) + + +class Tokenizer: + """Replaces any CSS comments with string tokens and vice versa.""" + + def __init__(self, token_re, token_string): + """Constructor for the Tokenizer. + + Args: + token_re: A regex for the string to be replace by a token. + token_string: The string to put between token delimiters when tokenizing. + """ + logging.debug('Tokenizer::init token_string=%s' % token_string) + self.token_re = token_re + self.token_string = token_string + self.originals = [] + + def Tokenize(self, line): + """Replaces any string matching token_re in line with string tokens. + + By passing a function as an argument to the re.sub line below, we bypass + the usual rule where re.sub will only replace the left-most occurrence of + a match by calling the passed in function for each occurrence. + + Args: + line: A line to replace token_re matches in. + + Returns: + line: A line with token_re matches tokenized. + """ + line = self.token_re.sub(self.TokenizeMatches, line) + logging.debug('Tokenizer::Tokenize returns: %s' % line) + return line + + def DeTokenize(self, line): + """Replaces tokens with the original string. + + Args: + line: A line with tokens. + + Returns: + line with any tokens replaced by the original string. + """ + + # Put all of the comments back in by their comment token. + for i, original in enumerate(self.originals): + token = '%s%s_%s%s' % (TOKEN_DELIMITER, self.token_string, i + 1, + TOKEN_DELIMITER) + line = line.replace(token, original) + logging.debug('Tokenizer::DeTokenize i:%s w/%s' % (i, token)) + logging.debug('Tokenizer::DeTokenize returns: %s' % line) + return line + + def TokenizeMatches(self, m): + """Replaces matches with tokens and stores the originals. + + Args: + m: A match object. + + Returns: + A string token which replaces the CSS comment. + """ + logging.debug('Tokenizer::TokenizeMatches %s' % m.group(1)) + self.originals.append(m.group(1)) + return '%s%s_%s%s' % (TOKEN_DELIMITER, + self.token_string, + len(self.originals), + TOKEN_DELIMITER) + + +def FixBodyDirectionLtrAndRtl(line): + """Replaces ltr with rtl and vice versa ONLY in the body direction. + + Args: + line: A string to replace instances of ltr with rtl. + Returns: + line with direction: ltr and direction: rtl swapped only in body selector. + line = FixBodyDirectionLtrAndRtl('body { direction:ltr }') + line will now be 'body { direction:rtl }'. + """ + + line = BODY_DIRECTION_LTR_RE.sub('\\1\\2\\3%s' % TMP_TOKEN, line) + line = BODY_DIRECTION_RTL_RE.sub('\\1\\2\\3%s' % LTR, line) + line = line.replace(TMP_TOKEN, RTL) + logging.debug('FixBodyDirectionLtrAndRtl returns: %s' % line) + return line + + +def FixLeftAndRight(line): + """Replaces left with right and vice versa in line. + + Args: + line: A string in which to perform the replacement. + + Returns: + line with left and right swapped. For example: + line = FixLeftAndRight('padding-left: 2px; margin-right: 1px;') + line will now be 'padding-right: 2px; margin-left: 1px;'. + """ + + line = LEFT_RE.sub(TMP_TOKEN, line) + line = RIGHT_RE.sub(LEFT, line) + line = line.replace(TMP_TOKEN, RIGHT) + logging.debug('FixLeftAndRight returns: %s' % line) + return line + + +def FixLeftAndRightInUrl(line): + """Replaces left with right and vice versa ONLY within background urls. + + Args: + line: A string in which to replace left with right and vice versa. + + Returns: + line with left and right swapped in the url string. For example: + line = FixLeftAndRightInUrl('background:url(right.png)') + line will now be 'background:url(left.png)'. + """ + + line = LEFT_IN_URL_RE.sub(TMP_TOKEN, line) + line = RIGHT_IN_URL_RE.sub(LEFT, line) + line = line.replace(TMP_TOKEN, RIGHT) + logging.debug('FixLeftAndRightInUrl returns: %s' % line) + return line + + +def FixLtrAndRtlInUrl(line): + """Replaces ltr with rtl and vice versa ONLY within background urls. + + Args: + line: A string in which to replace ltr with rtl and vice versa. + + Returns: + line with left and right swapped. For example: + line = FixLtrAndRtlInUrl('background:url(rtl.png)') + line will now be 'background:url(ltr.png)'. + """ + + line = LTR_IN_URL_RE.sub(TMP_TOKEN, line) + line = RTL_IN_URL_RE.sub(LTR, line) + line = line.replace(TMP_TOKEN, RTL) + logging.debug('FixLtrAndRtlInUrl returns: %s' % line) + return line + + +def FixCursorProperties(line): + """Fixes directional CSS cursor properties. + + Args: + line: A string to fix CSS cursor properties in. + + Returns: + line reformatted with the cursor properties substituted. For example: + line = FixCursorProperties('cursor: ne-resize') + line will now be 'cursor: nw-resize'. + """ + + line = CURSOR_EAST_RE.sub('\\1' + TMP_TOKEN, line) + line = CURSOR_WEST_RE.sub('\\1e-resize', line) + line = line.replace(TMP_TOKEN, 'w-resize') + logging.debug('FixCursorProperties returns: %s' % line) + return line + + +def FixFourPartNotation(line): + """Fixes the second and fourth positions in 4 part CSS notation. + + Args: + line: A string to fix 4 part CSS notation in. + + Returns: + line reformatted with the 4 part notations swapped. For example: + line = FixFourPartNotation('padding: 1px 2px 3px 4px') + line will now be 'padding: 1px 4px 3px 2px'. + """ + line = FOUR_NOTATION_QUANTITY_RE.sub('\\1 \\4 \\3 \\2', line) + line = FOUR_NOTATION_COLOR_RE.sub('\\1\\2 \\5 \\4 \\3', line) + logging.debug('FixFourPartNotation returns: %s' % line) + return line + + +def FixBackgroundPosition(line): + """Fixes horizontal background percentage values in line. + + Args: + line: A string to fix horizontal background position values in. + + Returns: + line reformatted with the 4 part notations swapped. + """ + line = BG_HORIZONTAL_PERCENTAGE_RE.sub(CalculateNewBackgroundPosition, line) + line = BG_HORIZONTAL_PERCENTAGE_X_RE.sub(CalculateNewBackgroundPositionX, + line) + logging.debug('FixBackgroundPosition returns: %s' % line) + return line + + +def CalculateNewBackgroundPosition(m): + """Fixes horizontal background-position percentages. + + This function should be used as an argument to re.sub since it needs to + perform replacement specific calculations. + + Args: + m: A match object. + + Returns: + A string with the horizontal background position percentage fixed. + BG_HORIZONTAL_PERCENTAGE_RE.sub(FixBackgroundPosition, + 'background-position: 75% 50%') + will return 'background-position: 25% 50%'. + """ + + # The flipped value is the offset from 100% + new_x = str(100-int(m.group(4))) + + # Since m.group(1) may very well be None type and we need a string.. + if m.group(1): + position_string = m.group(1) + else: + position_string = '' + + return 'background%s%s%s%s%%%s' % (position_string, m.group(2), m.group(3), + new_x, m.group(5)) + + +def CalculateNewBackgroundPositionX(m): + """Fixes percent based background-position-x. + + This function should be used as an argument to re.sub since it needs to + perform replacement specific calculations. + + Args: + m: A match object. + + Returns: + A string with the background-position-x percentage fixed. + BG_HORIZONTAL_PERCENTAGE_X_RE.sub(CalculateNewBackgroundPosition, + 'background-position-x: 75%') + will return 'background-position-x: 25%'. + """ + + # The flipped value is the offset from 100% + new_x = str(100-int(m.group(2))) + + return 'background-position-x%s%s%%' % (m.group(1), new_x) + + +def ChangeLeftToRightToLeft(lines, + swap_ltr_rtl_in_url=None, + swap_left_right_in_url=None): + """Turns lines into a stream and runs the fixing functions against it. + + Args: + lines: An list of CSS lines. + swap_ltr_rtl_in_url: Overrides this flag if param is set. + swap_left_right_in_url: Overrides this flag if param is set. + + Returns: + The same lines, but with left and right fixes. + """ + + global FLAGS + + # Possibly override flags with params. + logging.debug('ChangeLeftToRightToLeft swap_ltr_rtl_in_url=%s, ' + 'swap_left_right_in_url=%s' % (swap_ltr_rtl_in_url, + swap_left_right_in_url)) + if swap_ltr_rtl_in_url is None: + swap_ltr_rtl_in_url = FLAGS['swap_ltr_rtl_in_url'] + if swap_left_right_in_url is None: + swap_left_right_in_url = FLAGS['swap_left_right_in_url'] + + # Turns the array of lines into a single line stream. + logging.debug('LINES COUNT: %s' % len(lines)) + line = TOKEN_LINES.join(lines) + + # Tokenize any single line rules with the /* noflip */ annotation. + noflip_single_tokenizer = Tokenizer(NOFLIP_SINGLE_RE, 'NOFLIP_SINGLE') + line = noflip_single_tokenizer.Tokenize(line) + + # Tokenize any class rules with the /* noflip */ annotation. + noflip_class_tokenizer = Tokenizer(NOFLIP_CLASS_RE, 'NOFLIP_CLASS') + line = noflip_class_tokenizer.Tokenize(line) + + # Tokenize the comments so we can preserve them through the changes. + comment_tokenizer = Tokenizer(COMMENT_RE, 'C') + line = comment_tokenizer.Tokenize(line) + + # Here starteth the various left/right orientation fixes. + line = FixBodyDirectionLtrAndRtl(line) + + if swap_left_right_in_url: + line = FixLeftAndRightInUrl(line) + + if swap_ltr_rtl_in_url: + line = FixLtrAndRtlInUrl(line) + + line = FixLeftAndRight(line) + line = FixCursorProperties(line) + line = FixFourPartNotation(line) + line = FixBackgroundPosition(line) + + # DeTokenize the single line noflips. + line = noflip_single_tokenizer.DeTokenize(line) + + # DeTokenize the class-level noflips. + line = noflip_class_tokenizer.DeTokenize(line) + + # DeTokenize the comments. + line = comment_tokenizer.DeTokenize(line) + + # Rejoin the lines back together. + lines = line.split(TOKEN_LINES) + + return lines + +def usage(): + """Prints out usage information.""" + + print 'Usage:' + print ' ./cssjanus.py < file.css > file-rtl.css' + print 'Flags:' + print ' --swap_left_right_in_url: Fixes "left"/"right" string within urls.' + print ' Ex: ./cssjanus.py --swap_left_right_in_url < file.css > file_rtl.css' + print ' --swap_ltr_rtl_in_url: Fixes "ltr"/"rtl" string within urls.' + print ' Ex: ./cssjanus --swap_ltr_rtl_in_url < file.css > file_rtl.css' + +def setflags(opts): + """Parse the passed in command line arguments and set the FLAGS global. + + Args: + opts: getopt iterable intercepted from argv. + """ + + global FLAGS + + # Parse the arguments. + for opt, arg in opts: + logging.debug('opt: %s, arg: %s' % (opt, arg)) + if opt in ("-h", "--help"): + usage() + sys.exit() + elif opt in ("-d", "--debug"): + logging.getLogger().setLevel(logging.DEBUG) + elif opt == '--swap_ltr_rtl_in_url': + FLAGS['swap_ltr_rtl_in_url'] = True + elif opt == '--swap_left_right_in_url': + FLAGS['swap_left_right_in_url'] = True + + +def main(argv): + """Sends stdin lines to ChangeLeftToRightToLeft and writes to stdout.""" + + # Define the flags. + try: + opts, args = getopt.getopt(argv, 'hd', ['help', 'debug', + 'swap_left_right_in_url', + 'swap_ltr_rtl_in_url']) + except getopt.GetoptError: + usage() + sys.exit(2) + + # Parse and set the flags. + setflags(opts) + + # Call the main routine with all our functionality. + fixed_lines = ChangeLeftToRightToLeft(sys.stdin.readlines()) + sys.stdout.write(''.join(fixed_lines)) + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/skins/vector/cssjanus/csslex.py b/skins/vector/cssjanus/csslex.py new file mode 100644 index 00000000..1fc7304e --- /dev/null +++ b/skins/vector/cssjanus/csslex.py @@ -0,0 +1,114 @@ +#!/usr/bin/python +# +# Copyright 2007 Google Inc. All Rights Reserved. + +"""CSS Lexical Grammar rules. + +CSS lexical grammar from http://www.w3.org/TR/CSS21/grammar.html +""" + +__author__ = ['elsigh@google.com (Lindsey Simon)', + 'msamuel@google.com (Mike Samuel)'] + +# public symbols +__all__ = [ "NEWLINE", "HEX", "NON_ASCII", "UNICODE", "ESCAPE", "NMSTART", "NMCHAR", "STRING1", "STRING2", "IDENT", "NAME", "HASH", "NUM", "STRING", "URL", "SPACE", "WHITESPACE", "COMMENT", "QUANTITY", "PUNC" ] + +# The comments below are mostly copied verbatim from the grammar. + +# "@import" {return IMPORT_SYM;} +# "@page" {return PAGE_SYM;} +# "@media" {return MEDIA_SYM;} +# "@charset" {return CHARSET_SYM;} +KEYWORD = r'(?:\@(?:import|page|media|charset))' + +# nl \n|\r\n|\r|\f ; a newline +NEWLINE = r'\n|\r\n|\r|\f' + +# h [0-9a-f] ; a hexadecimal digit +HEX = r'[0-9a-f]' + +# nonascii [\200-\377] +NON_ASCII = r'[\200-\377]' + +# unicode \\{h}{1,6}(\r\n|[ \t\r\n\f])? +UNICODE = r'(?:(?:\\' + HEX + r'{1,6})(?:\r\n|[ \t\r\n\f])?)' + +# escape {unicode}|\\[^\r\n\f0-9a-f] +ESCAPE = r'(?:' + UNICODE + r'|\\[^\r\n\f0-9a-f])' + +# nmstart [_a-z]|{nonascii}|{escape} +NMSTART = r'(?:[_a-z]|' + NON_ASCII + r'|' + ESCAPE + r')' + +# nmchar [_a-z0-9-]|{nonascii}|{escape} +NMCHAR = r'(?:[_a-z0-9-]|' + NON_ASCII + r'|' + ESCAPE + r')' + +# ident -?{nmstart}{nmchar}* +IDENT = r'-?' + NMSTART + NMCHAR + '*' + +# name {nmchar}+ +NAME = NMCHAR + r'+' + +# hash +HASH = r'#' + NAME + +# string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\" ; "string" +STRING1 = r'"(?:[^\"\\]|\\.)*"' + +# string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\' ; 'string' +STRING2 = r"'(?:[^\'\\]|\\.)*'" + +# string {string1}|{string2} +STRING = '(?:' + STRING1 + r'|' + STRING2 + ')' + +# num [0-9]+|[0-9]*"."[0-9]+ +NUM = r'(?:[0-9]*\.[0-9]+|[0-9]+)' + +# s [ \t\r\n\f] +SPACE = r'[ \t\r\n\f]' + +# w {s}* +WHITESPACE = '(?:' + SPACE + r'*)' + +# url special chars +URL_SPECIAL_CHARS = r'[!#$%&*-~]' + +# url chars ({url_special_chars}|{nonascii}|{escape})* +URL_CHARS = r'(?:%s|%s|%s)*' % (URL_SPECIAL_CHARS, NON_ASCII, ESCAPE) + +# url +URL = r'url\(%s(%s|%s)%s\)' % (WHITESPACE, STRING, URL_CHARS, WHITESPACE) + +# comments +# see http://www.w3.org/TR/CSS21/grammar.html +COMMENT = r'/\*[^*]*\*+([^/*][^*]*\*+)*/' + +# {E}{M} {return EMS;} +# {E}{X} {return EXS;} +# {P}{X} {return LENGTH;} +# {C}{M} {return LENGTH;} +# {M}{M} {return LENGTH;} +# {I}{N} {return LENGTH;} +# {P}{T} {return LENGTH;} +# {P}{C} {return LENGTH;} +# {D}{E}{G} {return ANGLE;} +# {R}{A}{D} {return ANGLE;} +# {G}{R}{A}{D} {return ANGLE;} +# {M}{S} {return TIME;} +# {S} {return TIME;} +# {H}{Z} {return FREQ;} +# {K}{H}{Z} {return FREQ;} +# % {return PERCENTAGE;} +UNIT = r'(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)' + +# {num}{UNIT|IDENT} {return NUMBER;} +QUANTITY = '%s(?:%s%s|%s)?' % (NUM, WHITESPACE, UNIT, IDENT) + +# "" {return CDC;} +# "~=" {return INCLUDES;} +# "|=" {return DASHMATCH;} +# {w}"{" {return LBRACE;} +# {w}"+" {return PLUS;} +# {w}">" {return GREATER;} +# {w}"," {return COMMA;} +PUNC = r'|~=|\|=|[\{\+>,:;]' diff --git a/skins/vector/experiments/babaco-colors-a.css b/skins/vector/experiments/babaco-colors-a.css new file mode 100644 index 00000000..ce6f67dd --- /dev/null +++ b/skins/vector/experiments/babaco-colors-a.css @@ -0,0 +1,109 @@ +/* Babaco Color Scheme A */ + + +a:visited, +a:visited div.vectorTabs li.selected a:visited span { + color: #260e9c; +} + +html .thumbimage, +#toc, .toc, .mw-warning, div.thumbinner { + border-color: #cccccc; + background-color: #f7f7f7; +} + +/* Framework */ +#mw-page-base { + background-color: inherit !important; + background-image: none !important; +} +body { + background-color: #f9f9f9 !important; + background-image:url(images/page-base-updated.png); +} + +/* Links */ +a { + color: #0066cc; +} +a:visited { + color: #004d99; +} +a:active { + color: #ff6600; +} +a.stub { + color: #56228b; +} +a.new, #p-personal a.new { + color: #a31205 !important; +} +a.new:visited, #p-personal a.new:visited { + color: #a31205; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + border-color:#999999; + font-family:georgia, times, serif; + font-weight:bold; +} +#firstHeading { + font-size:1.5em; +} +h2 .editsection, +.portal h5{ + font-family:sans-serif; + font-weight:normal; + +} +#toc h2, .toc h2 { + font-family:sans-serif; + font-weight:normal; +} +body #mw-panel div.portal div.body { + background-image:url(images/new-portal-break-ltr.png); +} +body.rtl #mw-panel div.portal div.body { + background-image:url(images/new-portal-break-rtl.png); +} +body div.vectorTabs li a, div.vectorTabs li a span{ + color:#4d4d4d; +} +body div.vectorTabs li.selected a, +body div.vectorTabs li.selected a span, +body div.vectorTabs li.selected a:visited +body div.vectorTabs li.selected a:visited span { + color: #ff9900 !important; + font-weight:bold; +} +div.vectorTabs li.new a, +div.vectorTabs li.new a span, +div.vectorTabs li.new a:visited, +div.vectorTabs li.new a:visited span { + color:#a31205; +} +#toc, +.toc, +.mw-warning, +div.gallerybox div.thumb, +table.gallery, +#preferences fieldset.prefsection fieldset, +#preferences, +html .thumbimage, +.usermessage, +img.thumbborder, +div.thumbinner{ + border: 1px solid #cccccc; + background-color: #f7f7f7; +} +#mw-panel div.portal h5 { + font-weight:bold; + margin-bottom:0; + padding-bottom:0.05em; + color:#000000; +} diff --git a/skins/vector/experiments/babaco-colors-b.css b/skins/vector/experiments/babaco-colors-b.css new file mode 100644 index 00000000..227e197c --- /dev/null +++ b/skins/vector/experiments/babaco-colors-b.css @@ -0,0 +1,67 @@ +/* Babaco Color Scheme A */ + + +html .thumbimage, +#toc, .toc, .mw-warning, div.thumbinner { + border-color: #cccccc; + background-color: #f7f7f7; +} + +/* Framework */ +#mw-page-base { + background-color: inherit !important; + background-image: none !important; +} +body { + background-color: #f9f9f9 !important; + background-image:url(images/page-base-updated.png); +} +/* Links */ +a { + color: #003cb3; +} +a.stub { + color: #772233; +} +a.new, #p-personal a.new { + color: #a31205 !important; +} +{ + color: #260e9c; +} +a:visited, +a:visited div.vectorTabs li.selected a:visited span, +a.new:visited, +#p-personal a.new:visited { + color: #260e9c; +} +h1, +h2, +h3, +h4, +h5, +h6 { + border-color:#999999; +} + +div.vectorTabs li.new a, +div.vectorTabs li.new a span, +div.vectorTabs li.new a:visited, +div.vectorTabs li.new a:visited span { + color:#a31205; +} + +#toc, +.toc, +.mw-warning, +div.gallerybox div.thumb, +table.gallery, +#preferences fieldset.prefsection fieldset, +#preferences, +html .thumbimage, +.usermessage, +img.thumbborder, +div.thumbinner{ + border: 1px solid #cccccc; + background-color: #f7f7f7; +} diff --git a/skins/vector/experiments/babaco-colors-c.css b/skins/vector/experiments/babaco-colors-c.css new file mode 100644 index 00000000..d2dabf7d --- /dev/null +++ b/skins/vector/experiments/babaco-colors-c.css @@ -0,0 +1,91 @@ +/* Babaco Color Scheme C */ + +/* ridding ourselves of the gradient */ +#mw-page-base { + background-color: inherit !important; + background-image: none !important; +} + +a:visited, +a:visited div.vectorTabs li.selected a:visited span { + color: #260e9c; +} + +html .thumbimage, +#toc, .toc, .mw-warning, div.thumbinner { + border-color: #cccccc; + background-color: #f7f7f7; +} + +/* Framework */ +body { + background-color: #f9f9f9 !important; + background-image:url(images/page-base-updated.png); +} + +/* Links */ +a { + color: #0066cc; +} +a:visited { + color: #004d99; +} +a:active { + color: #ff6600; +} +a.stub { + color: #56228b; +} +a.new, #p-personal a.new { + color: #a31205 !important; +} +a.new:visited, #p-personal a.new:visited { + color: #a31205; +} + +#firstHeading { + font-size:1.5em; +} +h2 .editsection, +.portal h5 { + font-weight:normal; +} +#toc h2, .toc h2 { + font-weight:normal; +} +body #mw-panel div.portal div.body { + background-image:url(images/new-portal-break-ltr.png); +} + +div.vectorTabs li.new a, +div.vectorTabs li.new a span, +div.vectorTabs li.new a:visited, +div.vectorTabs li.new a:visited span { + color:#a31205; +} +#toc, +.toc, +.mw-warning, +div.gallerybox div.thumb, +table.gallery, +#preferences fieldset.prefsection fieldset, +#preferences, +html .thumbimage, +.usermessage, +img.thumbborder, +div.thumbinner { + border: 1px solid #cccccc; + background-color: #f7f7f7; +} +#mw-panel div.portal h5 { + font-weight:bold; + margin-bottom:0; + padding-bottom:0.05em; + color:#000000; +} +div.vectorTabs li.selected a, +div.vectorTabs li.selected a span, +div.vectorTabs li.selected a:visited +div.vectorTabs li.selected a:visited span { + color: #333333 !important; +} \ No newline at end of file diff --git a/skins/vector/experiments/images/new-border.png b/skins/vector/experiments/images/new-border.png new file mode 100644 index 00000000..735324ef Binary files /dev/null and b/skins/vector/experiments/images/new-border.png differ diff --git a/skins/vector/experiments/images/new-portal-break-ltr.png b/skins/vector/experiments/images/new-portal-break-ltr.png new file mode 100644 index 00000000..cd8f3b15 Binary files /dev/null and b/skins/vector/experiments/images/new-portal-break-ltr.png differ diff --git a/skins/vector/experiments/images/new-portal-break-rtl.png b/skins/vector/experiments/images/new-portal-break-rtl.png new file mode 100644 index 00000000..45c5b2f9 Binary files /dev/null and b/skins/vector/experiments/images/new-portal-break-rtl.png differ diff --git a/skins/vector/experiments/images/page-base-fade.png b/skins/vector/experiments/images/page-base-fade.png new file mode 100644 index 00000000..dc631823 Binary files /dev/null and b/skins/vector/experiments/images/page-base-fade.png differ diff --git a/skins/vector/experiments/images/page-base-updated.png b/skins/vector/experiments/images/page-base-updated.png new file mode 100644 index 00000000..54ffeb00 Binary files /dev/null and b/skins/vector/experiments/images/page-base-updated.png differ diff --git a/skins/vector/experiments/images/tab-active-first.png b/skins/vector/experiments/images/tab-active-first.png new file mode 100644 index 00000000..e4c39c42 Binary files /dev/null and b/skins/vector/experiments/images/tab-active-first.png differ diff --git a/skins/vector/experiments/images/tab-active-last.png b/skins/vector/experiments/images/tab-active-last.png new file mode 100644 index 00000000..a96f3916 Binary files /dev/null and b/skins/vector/experiments/images/tab-active-last.png differ diff --git a/skins/vector/experiments/images/tab-fade.png b/skins/vector/experiments/images/tab-fade.png new file mode 100644 index 00000000..1eb0e23b Binary files /dev/null and b/skins/vector/experiments/images/tab-fade.png differ diff --git a/skins/vector/experiments/images/tab-first.png b/skins/vector/experiments/images/tab-first.png new file mode 100644 index 00000000..439b713f Binary files /dev/null and b/skins/vector/experiments/images/tab-first.png differ diff --git a/skins/vector/experiments/images/tab-last.png b/skins/vector/experiments/images/tab-last.png new file mode 100644 index 00000000..08e283dd Binary files /dev/null and b/skins/vector/experiments/images/tab-last.png differ diff --git a/skins/vector/experiments/images/tab-new-fade.png b/skins/vector/experiments/images/tab-new-fade.png new file mode 100644 index 00000000..44925505 Binary files /dev/null and b/skins/vector/experiments/images/tab-new-fade.png differ diff --git a/skins/vector/experiments/new-tabs.css b/skins/vector/experiments/new-tabs.css new file mode 100644 index 00000000..e3850e8e --- /dev/null +++ b/skins/vector/experiments/new-tabs.css @@ -0,0 +1,322 @@ +/* new border color */ +#content { + background-image: url(images/new-border.png); +} + +#footer { + background-image: url(images/new-border.png); +} + body div#left-navigation, + body div#right-navigation { + top:3.2em; + } + body div#right-navigation { + margin-top:3.2em; + } + body #p-search form, + body #p-search input, + body #simpleSearch { + margin-top:0; + } + body div#p-cactions { + margin-top:0; + } + /* Namespaces and Views */ + /* @noflip */ + div.vectorTabs { + float: left; + } + body div.vectorTabs { + background-image: none; + padding-left: 0; + } + /* @noflip */ + div.vectorTabs ul { + float: left; + } + div.vectorTabs ul { + height: 100%; + list-style: none; + background-image:none; + margin: 0; + padding: 0; + } + /* @noflip */ + div.vectorTabs ul li { + float: left; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + body div.vectorTabs ul li { + line-height: 1em; + display: inline-block; + height: 2em; + margin: 0 1px 0 0; + padding: 0; + background:none; + overflow:hidden; + white-space:nowrap; + } + /* IGNORED BY IE6 */ + div.vectorTabs ul > li { + display: block; + } + body div.vectorTabs li.selected { + background-image: none; + border:none; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + body div.vectorTabs li a { + border-top:1px solid #cccccc; + border-left:1px solid #cccccc; + border-right:1px solid #cccccc; + display: inline-block; + height: 1.7em; + padding-left: 0.6em; + padding-right: 0.6em; + background-image:url(images/tab-fade.png); + background-position:bottom left; + background-repeat:repeat-x; + background-color:#ffffff; + } + body div.vectorTabs li.new a{ + background-image:url(images/tab-new-fade.png); + + } + div.vectorTabs li a, + div.vectorTabs li a span { + cursor: pointer; + } + div.vectorTabs li a span { + font-size: 0.8em; + } + /* IGNORED BY IE6 */ + div.vectorTabs li > a { + display: block; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + body div.vectorTabs a span { + display: inline-block; + padding-top: 0.5em; + } + /* IGNORED BY IE6 */ + /* @noflip */ + div.vectorTabs a > span { + float: left; + display: block; + } + body div.vectorTabs li.last { + background-image: url(images/tab-last.png); + background-repeat:no-repeat; + background-position:top right; + border:none; + } + body div.vectorTabs li.last a { + margin-right:7px; + padding-left:0.4em; + padding-right:0; + border-left:1px solid #cccccc; + border-top:1px solid #cccccc; + border-right:none; + background-image:url(images/tab-fade.png); + background-position:top left; + background-repeat:repeat-x; + } + body div.vectorTabs li.first { + background-image: url(images/tab-first.png); + background-repeat:no-repeat; + background-position:top left; + border:none; + } + body div.vectorTabs li.first a { + margin-left:7px; + padding-left:0em; + padding-right:0.4em; + border-right:1px solid #cccccc; + border-top:1px solid #cccccc; + background-image:url(images/tab-fade.png); + background-position:top left; + background-repeat:repeat-x; + } + + div.vectorTabs li.selected a, + div.vectorTabs li.selected a span, + div.vectorTabs li.selected a:visited + div.vectorTabs li.selected a:visited span { + color: #be5900 !important; + text-decoration: none; + } + + body div.vectorTabs li.selected a { + border-top:1px solid #6cc8f3; + border-right:1px solid #6cc8f3; + border-left:1px solid #6cc8f3; + background-color:#fff; + height:1.75em; + background-image:none; + } + body div.vectorTabs li.selected.first { + background-image: url(images/tab-active-first.png); + background-repeat:no-repeat; + background-position:top left; + } + body div.vectorTabs li.selected.first a { + margin-left:7px; + padding-right:0.6em; + padding-left:0.4em; + border-left:none; + } + body div.vectorTabs li.selected.last { + background-image: url(images/tab-active-last.png); + background-repeat:no-repeat; + background-position:top right; + } + body div.vectorTabs li.selected.last a { + margin-right:7px; + padding-left:0.6em; + padding-right:0.4em; + border-right:none; + } + + /* Variants and Actions */ + /* @noflip */ + div.vectorMenu { + background-image:url(images/tab-fade.png); + background-position:bottom left; + background-repeat:repeat-x; + border-top:1px solid #cccccc; + border-left:1px solid #cccccc; + border-right:1px solid #cccccc; + } + body.rtl div.vectorMenu { + direction: rtl; + } + /* @noflip */ + body #mw-head div.vectorMenu h5 { + background-image: url(../images/arrow-down-icon.png); + background-position: center center; + background-repeat: no-repeat; + padding-left: 0; + margin-left: 0px; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + body div.vectorMenu h5 a { + display: inline-block; + width: 24px; + height:1.5em; + background-image: none !important; + + } + /* IGNORED BY IE6 */ + div.vectorMenu h5 > a { + display: block; + } + div.vectorMenu div.menu { + position: relative; + left:1px; + display: none; + clear: both; + text-align: left; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + body.rtl div.vectorMenu div.menu { + margin-right: 24px; + } + /* IGNORED BY IE6 */ + body.rtl div.vectorMenu > div.menu { + margin-right: auto; + } + /* Fixes old versions of FireFox */ + body.rtl div.vectorMenu > div.menu, + x:-moz-any-link { + margin-right: 24px; + } + div.vectorMenu:hover div.menu { + display: block; + } + div.vectorMenu ul { + position: absolute; + background-color: white; + border: solid 1px silver; + border-top-width: 0; + list-style: none; + list-style-image: none; + list-style-type: none; + padding: 0; + margin: 0; + margin-left: -1px; + text-align: left; + } + /* Fixes old versions of FireFox */ + div.vectorMenu ul, + x:-moz-any-link { + min-width: 5em; + } + /* Returns things back to normal in modern versions of FireFox */ + div.vectorMenu ul, + x:-moz-any-link, + x:default { + min-width: 0; + } + div.vectorMenu li { + padding: 0; + margin: 0; + text-align: left; + line-height: 1em; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorMenu li a { + display: inline-block; + padding: 0.5em; + white-space: nowrap; + } + /* IGNORED BY IE6 */ + div.vectorMenu li > a { + display: block; + } + div.vectorMenu li a { + color: #0645ad; + cursor: pointer; + font-size: 0.8em; + } + div.vectorMenu li.selected a, + div.vectorMenu li.selected a:visited { + color: #333333; + text-decoration: none; + } +#ca-unwatch.icon, +#ca-watch.icon { + background-color:#ffffff; + height:1.75em !important; + background-image:url(images/tab-fade.png); + background-position:bottom left; + background-repeat:repeat-x; +} +#ca-unwatch.icon a, +#ca-watch.icon a { + height: 1.7em !important; + border-bottom:none; + background-color:transparent; +} +#ca-watch.icon a, +#ca-unwatch.icon a { + background-repeat:no-repeat; +} +.wikiEditor-ui-tabs { + border: none; + height: 2.15em; +} +.wikiEditor-ui-buttons { + height: 2.15em; +} +.wikiEditor-ui-tabs div { + border: 1px solid silver; + margin-right: 1px; + height: 2.15em; +} +.wikiEditor-ui-tabs div a { + line-height: 2.15em; + background: #FFFFFF url(images/tab-fade.png) repeat-x 0 100%; +} +.wikiEditor-ui-tabs div.current a { + background: #FFFFFF; +} diff --git a/skins/vector/images/arrow-down-icon.png b/skins/vector/images/arrow-down-icon.png new file mode 100644 index 00000000..bf2d4fb4 Binary files /dev/null and b/skins/vector/images/arrow-down-icon.png differ diff --git a/skins/vector/images/audio-icon.png b/skins/vector/images/audio-icon.png new file mode 100644 index 00000000..0f59a2bb Binary files /dev/null and b/skins/vector/images/audio-icon.png differ diff --git a/skins/vector/images/border.png b/skins/vector/images/border.png new file mode 100644 index 00000000..54b47922 Binary files /dev/null and b/skins/vector/images/border.png differ diff --git a/skins/vector/images/bullet-icon.png b/skins/vector/images/bullet-icon.png new file mode 100644 index 00000000..e304b267 Binary files /dev/null and b/skins/vector/images/bullet-icon.png differ diff --git a/skins/vector/images/document-icon.png b/skins/vector/images/document-icon.png new file mode 100644 index 00000000..91dc16f6 Binary files /dev/null and b/skins/vector/images/document-icon.png differ diff --git a/skins/vector/images/edit-icon.png b/skins/vector/images/edit-icon.png new file mode 100644 index 00000000..4a962767 Binary files /dev/null and b/skins/vector/images/edit-icon.png differ diff --git a/skins/vector/images/external-link-ltr-icon.png b/skins/vector/images/external-link-ltr-icon.png new file mode 100644 index 00000000..4b710b03 Binary files /dev/null and b/skins/vector/images/external-link-ltr-icon.png differ diff --git a/skins/vector/images/external-link-rtl-icon.png b/skins/vector/images/external-link-rtl-icon.png new file mode 100644 index 00000000..17df03a3 Binary files /dev/null and b/skins/vector/images/external-link-rtl-icon.png differ diff --git a/skins/vector/images/file-icon.png b/skins/vector/images/file-icon.png new file mode 100644 index 00000000..1261f00c Binary files /dev/null and b/skins/vector/images/file-icon.png differ diff --git a/skins/vector/images/link-icon.png b/skins/vector/images/link-icon.png new file mode 100644 index 00000000..fc77e81d Binary files /dev/null and b/skins/vector/images/link-icon.png differ diff --git a/skins/vector/images/lock-icon.png b/skins/vector/images/lock-icon.png new file mode 100644 index 00000000..9e63807a Binary files /dev/null and b/skins/vector/images/lock-icon.png differ diff --git a/skins/vector/images/magnify-clip.png b/skins/vector/images/magnify-clip.png new file mode 100644 index 00000000..00a9cee1 Binary files /dev/null and b/skins/vector/images/magnify-clip.png differ diff --git a/skins/vector/images/mail-icon.png b/skins/vector/images/mail-icon.png new file mode 100644 index 00000000..50de0781 Binary files /dev/null and b/skins/vector/images/mail-icon.png differ diff --git a/skins/vector/images/news-icon.png b/skins/vector/images/news-icon.png new file mode 100644 index 00000000..8ab49957 Binary files /dev/null and b/skins/vector/images/news-icon.png differ diff --git a/skins/vector/images/page-base.png b/skins/vector/images/page-base.png new file mode 100644 index 00000000..17d02a74 Binary files /dev/null and b/skins/vector/images/page-base.png differ diff --git a/skins/vector/images/page-fade.png b/skins/vector/images/page-fade.png new file mode 100644 index 00000000..815a0486 Binary files /dev/null and b/skins/vector/images/page-fade.png differ diff --git a/skins/vector/images/portal-break-ltr.png b/skins/vector/images/portal-break-ltr.png new file mode 100644 index 00000000..c1823705 Binary files /dev/null and b/skins/vector/images/portal-break-ltr.png differ diff --git a/skins/vector/images/portal-break-rtl.png b/skins/vector/images/portal-break-rtl.png new file mode 100644 index 00000000..a45144c0 Binary files /dev/null and b/skins/vector/images/portal-break-rtl.png differ diff --git a/skins/vector/images/portal-break.png b/skins/vector/images/portal-break.png new file mode 100644 index 00000000..e81b5597 Binary files /dev/null and b/skins/vector/images/portal-break.png differ diff --git a/skins/vector/images/preferences-base.png b/skins/vector/images/preferences-base.png new file mode 100644 index 00000000..adfd70d7 Binary files /dev/null and b/skins/vector/images/preferences-base.png differ diff --git a/skins/vector/images/preferences-break.png b/skins/vector/images/preferences-break.png new file mode 100644 index 00000000..6c5c68c4 Binary files /dev/null and b/skins/vector/images/preferences-break.png differ diff --git a/skins/vector/images/preferences-edge.png b/skins/vector/images/preferences-edge.png new file mode 100644 index 00000000..3da0d099 Binary files /dev/null and b/skins/vector/images/preferences-edge.png differ diff --git a/skins/vector/images/preferences-fade.png b/skins/vector/images/preferences-fade.png new file mode 100644 index 00000000..b4773c5c Binary files /dev/null and b/skins/vector/images/preferences-fade.png differ diff --git a/skins/vector/images/search-fade.png b/skins/vector/images/search-fade.png new file mode 100644 index 00000000..53461d6d Binary files /dev/null and b/skins/vector/images/search-fade.png differ diff --git a/skins/vector/images/search-ltr.png b/skins/vector/images/search-ltr.png new file mode 100644 index 00000000..1db2eb24 Binary files /dev/null and b/skins/vector/images/search-ltr.png differ diff --git a/skins/vector/images/search-rtl.png b/skins/vector/images/search-rtl.png new file mode 100644 index 00000000..c26c8d07 Binary files /dev/null and b/skins/vector/images/search-rtl.png differ diff --git a/skins/vector/images/tab-break.png b/skins/vector/images/tab-break.png new file mode 100644 index 00000000..81155562 Binary files /dev/null and b/skins/vector/images/tab-break.png differ diff --git a/skins/vector/images/tab-current-fade.png b/skins/vector/images/tab-current-fade.png new file mode 100644 index 00000000..c6238d26 Binary files /dev/null and b/skins/vector/images/tab-current-fade.png differ diff --git a/skins/vector/images/tab-normal-fade.png b/skins/vector/images/tab-normal-fade.png new file mode 100644 index 00000000..4a5e3e4a Binary files /dev/null and b/skins/vector/images/tab-normal-fade.png differ diff --git a/skins/vector/images/talk-icon.png b/skins/vector/images/talk-icon.png new file mode 100644 index 00000000..0b80ee91 Binary files /dev/null and b/skins/vector/images/talk-icon.png differ diff --git a/skins/vector/images/user-icon.png b/skins/vector/images/user-icon.png new file mode 100644 index 00000000..ac3d59d5 Binary files /dev/null and b/skins/vector/images/user-icon.png differ diff --git a/skins/vector/images/video-icon.png b/skins/vector/images/video-icon.png new file mode 100644 index 00000000..5e7f4af7 Binary files /dev/null and b/skins/vector/images/video-icon.png differ diff --git a/skins/vector/images/watch-icon-loading.gif b/skins/vector/images/watch-icon-loading.gif new file mode 100644 index 00000000..618c308e Binary files /dev/null and b/skins/vector/images/watch-icon-loading.gif differ diff --git a/skins/vector/images/watch-icons.png b/skins/vector/images/watch-icons.png new file mode 100644 index 00000000..54b2c793 Binary files /dev/null and b/skins/vector/images/watch-icons.png differ diff --git a/skins/vector/main-ltr.css b/skins/vector/main-ltr.css new file mode 100644 index 00000000..78dd1e2b --- /dev/null +++ b/skins/vector/main-ltr.css @@ -0,0 +1,1128 @@ +/* + * main-rtl.css is automatically generated using CSSJanus, a python script for + * creating RTL versions of otherwise LTR stylesheets. + * + * You may download the tool to rebuild this stylesheet + * http://code.google.com/p/cssjanus/ + * + * An online version of this tool can be used at: + * http://cssjanus.commoner.com/ + * + * The following command is used to generate the RTL version of this file + * ./cssjanus.py --swap_ltr_rtl_in_url < main-ltr.css > main-rtl.css + * + * Any rules which should not be flipped should be prepended with @noflip in + * a comment block. + */ +/* Framework */ +html, +body { + height: 100%; + margin: 0; + padding: 0; + font-family: sans-serif; + font-size: 1em; +} +body { + background-color: #f3f3f3; + background-image: url(images/page-base.png); +} +/* Content */ +#content { + margin-left: 10em; + padding: 1em; + background-image: url(images/border.png); + background-position: top left; + background-repeat: repeat-y; + background-color: white; +} +/* Head */ +#mw-page-base { + height: 5em; + background-color: white; + background-image: url(images/page-fade.png); + background-position: bottom left; + background-repeat: repeat-x; +} +#mw-head-base { + margin-top: -5em; + margin-left: 10em; + height: 5em; + background-image: url(images/border.png); + background-position: bottom left; + background-repeat: repeat-x; +} +#mw-head { + position: absolute; + top: 0; + right: 0; + width: 100%; +} +#mw-head h5 { + margin: 0; + padding: 0; +} + /* Hide empty portlets */ + div.emptyPortlet { + display: none; + } + /* Personal */ + #p-personal { + position: absolute; + top: 0; + margin-left: 10em; + right: 0.75em; + } + #p-personal h5 { + display: none; + } + #p-personal ul { + list-style: none; + margin: 0; + padding: 0; + } + /* @noflip */ + #p-personal li { + line-height: 1.125em; + float: left; + } + #p-personal li { + margin-left: 0.75em; + margin-top: 0.5em; + font-size: 0.75em; + } + /* Navigation Containers */ + #left-navigation { + position: absolute; + left: 10em; + top: 2.5em; + } + #right-navigation { + float: right; + margin-top: 2.5em; + } + /* Navigation Labels */ + div.vectorTabs h5, + div.vectorMenu h5 span { + display: none; + } + /* Namespaces and Views */ + /* @noflip */ + div.vectorTabs { + float: left; + } + div.vectorTabs { + background-image: url(images/tab-break.png); + background-position: bottom left; + background-repeat: no-repeat; + padding-left: 1px; + } + /* @noflip */ + div.vectorTabs ul { + float: left; + } + div.vectorTabs ul { + height: 100%; + list-style: none; + margin: 0; + padding: 0; + } + /* @noflip */ + div.vectorTabs ul li { + float: left; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorTabs ul li { + line-height: 1.125em; + display: inline-block; + height: 100%; + margin: 0; + padding: 0; + background-color: #f3f3f3; + background-image: url(images/tab-normal-fade.png); + background-position: bottom left; + background-repeat: repeat-x; + white-space:nowrap; + } + /* IGNORED BY IE6 */ + div.vectorTabs ul > li { + display: block; + } + div.vectorTabs li.selected { + background-image: url(images/tab-current-fade.png); + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorTabs li a { + display: inline-block; + height: 2.5em; + padding-left: 0.4em; + padding-right: 0.4em; + background-image: url(images/tab-break.png); + background-position: bottom right; + background-repeat: no-repeat; + } + div.vectorTabs li a, + div.vectorTabs li a span { + color: #0645ad; + cursor: pointer; + } + div.vectorTabs li a span { + font-size: 0.8em; + } + /* IGNORED BY IE6 */ + div.vectorTabs li > a { + display: block; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorTabs a span { + display: inline-block; + padding-top: 1.25em; + } + /* IGNORED BY IE6 */ + /* @noflip */ + div.vectorTabs a > span { + float: left; + display: block; + } + div.vectorTabs li.selected a, + div.vectorTabs li.selected a span, + div.vectorTabs li.selected a:visited + div.vectorTabs li.selected a:visited span { + color: #333333; + text-decoration: none; + } + div.vectorTabs li.new a, + div.vectorTabs li.new a span, + div.vectorTabs li.new a:visited, + div.vectorTabs li.new a:visited span { + color: #a55858; + } + /* Variants and Actions */ + /* @noflip */ + div.vectorMenu { + direction: ltr; + float: left; + background-image: url(images/arrow-down-icon.png); + background-position: center center; + background-repeat: no-repeat; + } + /* @noflip */ + body.rtl div.vectorMenu { + direction: rtl; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + /* @noflip */ + #mw-head div.vectorMenu h5 { + float: left; + background-image: url(images/tab-break.png); + background-repeat: no-repeat; + } + /* IGNORED BY IE6 */ + #mw-head div.vectorMenu > h5 { + background-image: none; + } + #mw-head div.vectorMenu h5 { + background-position: bottom left; + margin-left: -1px; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + /* @noflip */ + div.vectorMenu h5 a { + display: inline-block; + width: 24px; + height: 2.5em; + text-decoration: none; + background-image: url(images/tab-break.png); + background-repeat: no-repeat; + } + div.vectorMenu h5 a{ + background-position: bottom right; + } + /* IGNORED BY IE6 */ + div.vectorMenu h5 > a { + display: block; + } + div.vectorMenu div.menu { + position: relative; + display: none; + clear: both; + text-align: left; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + /* @noflip */ + body.rtl div.vectorMenu div.menu { + margin-left: 24px; + } + /* IGNORED BY IE6 */ + /* @noflip */ + body.rtl div.vectorMenu > div.menu { + margin-left: auto; + } + /* Fixes old versions of FireFox */ + /* @noflip */ + body.rtl div.vectorMenu > div.menu, + x:-moz-any-link { + margin-left: 23px; + } + div.vectorMenu:hover div.menu { + display: block; + } + div.vectorMenu ul { + position: absolute; + background-color: white; + border: solid 1px silver; + border-top-width: 0; + list-style: none; + list-style-image: none; + list-style-type: none; + padding: 0; + margin: 0; + margin-left: -1px; + text-align: left; + } + /* Fixes old versions of FireFox */ + div.vectorMenu ul, + x:-moz-any-link { + min-width: 5em; + } + /* Returns things back to normal in modern versions of FireFox */ + div.vectorMenu ul, + x:-moz-any-link, + x:default { + min-width: 0; + } + div.vectorMenu li { + padding: 0; + margin: 0; + text-align: left; + line-height: 1em; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorMenu li a { + display: inline-block; + padding: 0.5em; + white-space: nowrap; + } + /* IGNORED BY IE6 */ + div.vectorMenu li > a { + display: block; + } + div.vectorMenu li a { + color: #0645ad; + cursor: pointer; + font-size: 0.8em; + } + div.vectorMenu li.selected a, + div.vectorMenu li.selected a:visited { + color: #333333; + text-decoration: none; + } + /* Search */ + #p-search h5 { + display: none; + } + /* @noflip */ + #p-search { + float: left; + } + #p-search { + margin-right: 0.5em; + margin-left: 0.5em; + } + #p-search form, + #p-search input { + margin: 0; + margin-top: 0.4em; + } + #simpleSearch { + margin-top: 0.5em; + position: relative; + border: solid 1px #AAAAAA; + background-color: white; + background-image: url(images/search-fade.png); + background-position: top left; + background-repeat: repeat-x; + } + #simpleSearch label { + font-size: 0.8em; + top: 0.25em; + } + #simpleSearch input#searchInput { + margin: 0; + border-width: 0; + padding: 0.25em; + line-height: 1em; + font-size: 0.8em; + width: 9em; + background-color: transparent; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + #simpleSearch button#searchButton { + margin: 0; + padding: 0; + width: 1.75em; + height: 1.5em; + border: none; + cursor: pointer; + background-color: transparent; + background-image: url(images/search-ltr.png); + background-position: center center; + background-repeat: no-repeat; + } + /* IGNORED BY IE6 */ + #simpleSearch > button#searchButton { + height: 100%; + } + .suggestions-special .special-label { + font-size: 0.8em; + color: gray; + } + .suggestions-special .special-query { + color: black; + font-style: italic; + } + .suggestions-special .special-hover { + background-color: silver; + } +/* Panel */ +#mw-panel { + position: absolute; + top: 160px; + padding-top: 1em; + width: 10em; + left: 0; +} + #mw-panel div.portal { + padding-bottom: 1.5em; + } + #mw-panel div.portal h5 { + font-weight: normal; + color: #444444; + padding: 0.25em; + padding-top: 0; + padding-left: 1.75em; + cursor: default; + border: none; + font-size: 0.75em; + } + #mw-panel div.portal div.body { + margin: 0; + padding-top: 0.5em; + margin-left: 1.25em; + background-image: url(images/portal-break.png); + background-repeat: no-repeat; + background-position: top left; + } + #mw-panel div.portal div.body ul { + list-style: none; + list-style-image: none; + list-style-type: none; + padding: 0; + margin: 0; + } + #mw-panel div.portal div.body ul li { + line-height: 1.125em; + padding: 0; + padding-bottom: 0.5em; + margin: 0; + overflow: hidden; + font-size: 0.75em; + } + #mw-panel div.portal div.body ul li a { + color: #0645ad; + } + #mw-panel div.portal div.body ul li a:visited { + color: #0b0080; + } +/* Footer */ +#footer { + margin-left: 10em; + margin-top: 0; + padding: 0.75em; + background-image: url(images/border.png); + background-position: top left; + background-repeat: repeat-x; +} +#footer ul { + list-style: none; + list-style-image: none; + list-style-type: none; + margin: 0; + padding: 0; +} +#footer ul li { + margin: 0; + padding: 0; + padding-top: 0.5em; + padding-bottom: 0.5em; + color: #333333; + font-size: 0.7em; +} +#footer #footer-icons { + float: right; +} +/* @noflip */ +body.ltr #footer #footer-places { + float: left; +} +#footer #footer-info li { + line-height: 1.4em; +} +#footer #footer-icons li { + float: left; + margin-left: 0.5em; + line-height: 2em; +} +#footer #footer-places li { + float: left; + margin-right: 1em; + line-height: 2em; +} +/* Logo */ +#p-logo { + position: absolute; + top: -160px; + left: 0; + width: 10em; + height: 160px; +} +#p-logo a { + display: block; + width: 10em; + height: 160px; + background-repeat: no-repeat; + background-position: center center; + text-decoration: none; +} + +/* + * + * The following code is highly modified from monobook. It would be nice if the + * preftoc id was more human readable like preferences-toc for instance, + * howerver this would require backporting the other skins. + */ + +/* Preferences */ +#preftoc { + /* Tabs */ + width: 100%; + float: left; + clear: both; + margin: 0 !important; + padding: 0 !important; + background-image: url(images/preferences-break.png); + background-position: bottom left; + background-repeat: no-repeat; +} + #preftoc li { + /* Tab */ + float: left; + margin: 0; + padding: 0; + padding-right: 1px; + height: 2.25em; + white-space: nowrap; + list-style-type: none; + list-style-image: none; + background-image: url(images/preferences-break.png); + background-position: bottom right; + background-repeat: no-repeat; + } + /* IGNORED BY IE6 */ + #preftoc li:first-child { + margin-left: 1px; + } + #preftoc a, + #preftoc a:active { + display: inline-block; + position: relative; + color: #0645ad; + padding: 0.5em; + text-decoration: none; + background-image: none; + font-size: 0.9em; + } + #preftoc a:hover { + text-decoration: underline; + } + #preftoc li.selected a { + background-image: url(images/preferences-fade.png); + background-position: bottom; + background-repeat: repeat-x; + color: #333333; + text-decoration: none; + } +#preferences { + float: left; + width: 100%; + margin: 0; + margin-top: -2px; + clear: both; + border: solid 1px #cccccc; + background-color: #f9f9f9; + background-image: url(images/preferences-base.png); +} +#preferences fieldset.prefsection { + border: none; + padding: 0; + margin: 1em; +} +#preferences fieldset.prefsection fieldset { + border: none; + border-top: solid 1px #cccccc; +} +#preferences legend { + color: #666666; +} +#preferences fieldset.prefsection legend.mainLegend { + display: none; +} +#preferences td { + padding-left: 0.5em; + padding-right: 0.5em; +} +#preferences td.htmlform-tip { + font-size: x-small; + padding: .2em 2em; + color: #666666; +} +#preferences div.mw-prefs-buttons { + padding: 1em; +} +#preferences div.mw-prefs-buttons input { + margin-right: 0.25em; +} + +/* + * Styles for the user login and create account forms + */ +#userlogin, #userloginForm { + border: solid 1px #cccccc; + padding: 1.2em; + margin: .5em; + float: left; +} + +#userlogin { + min-width: 20em; + max-width: 90%; + width: 40em; +} + +/* + * + * The following code is slightly modified from monobook + * + */ +#content { + line-height: 1.5em; +} +#bodyContent { + font-size: 0.8em; +} +/* Links */ +a { + text-decoration: none; + color: #0645ad; + background: none; +} +a:visited { + color: #0b0080; +} +a:active { + color: #faa700; +} +a:hover { + text-decoration: underline; +} +a.stub { + color: #772233; +} +a.new, #p-personal a.new { + color: #ba0000; +} +a.new:visited, #p-personal a.new:visited { + color: #a55858; +} + +/* Inline Elements */ +img { + border: none; + vertical-align: middle; +} +hr { + height: 1px; + color: #aaa; + background-color: #aaa; + border: 0; + margin: .2em 0 .2em 0; +} + +/* Structural Elements */ +h1, +h2, +h3, +h4, +h5, +h6 { + color: black; + background: none; + font-weight: normal; + margin: 0; + padding-top: .5em; + padding-bottom: .17em; + border-bottom: 1px solid #aaa; + width: auto; +} +h1 { font-size: 188%; } +h1 .editsection { font-size: 53%; } +h2 { font-size: 150%; } +h2 .editsection { font-size: 67%; } +h3, +h4, +h5, +h6 { + border-bottom: none; + font-weight: bold; +} +h3 { font-size: 132%; } +h3 .editsection { font-size: 76%; font-weight: normal; } +h4 { font-size: 116%; } +h4 .editsection { font-size: 86%; font-weight: normal; } +h5 { font-size: 100%; } +h5 .editsection { font-weight: normal; } +h6 { font-size: 80%; } +h6 .editsection { font-size: 125%; font-weight: normal; } +p { + margin: .4em 0 .5em 0; + line-height: 1.5em; +} + p img { + margin: 0; + } +abbr, +acronym, +.explain { + border-bottom: 1px dotted black; + color: black; + background: none; + cursor: help; +} +q { + font-family: Times, "Times New Roman", serif; + font-style: italic; +} +/* Disabled for now +blockquote { + font-family: Times, "Times New Roman", serif; + font-style: italic; +}*/ +code { + background-color: #f9f9f9; +} +pre { + padding: 1em; + border: 1px dashed #2f6fab; + color: black; + background-color: #f9f9f9; + line-height: 1.1em; +} +ul { + line-height: 1.5em; + list-style-type: square; + margin: .3em 0 0 1.5em; + padding: 0; + list-style-image: url(images/bullet-icon.png); +} +ol { + line-height: 1.5em; + margin: .3em 0 0 3.2em; + padding: 0; + list-style-image: none; +} +li { + margin-bottom: .1em; +} +dt { + font-weight: bold; + margin-bottom: .1em; +} +dl { + margin-top: .2em; + margin-bottom: .5em; +} +dd { + line-height: 1.5em; + margin-left: 2em; + margin-bottom: .1em; +} +/* Tables */ +table { + font-size: 100%; + color: black; + /* we don't want the bottom borders of

    s to be visible through + * floated tables */ + background-color: white; +} +fieldset table { + /* but keep table layouts in forms clean... */ + background: none; +} +/* Forms */ +fieldset { + border: 1px solid #2f6fab; + margin: 1em 0 1em 0; + padding: 0 1em 1em; + line-height: 1.5em; +} + fieldset.nested { + margin: 0 0 0.5em 0; + padding: 0 0.5em 0.5em; + } +legend { + padding: .5em; + font-size: 95%; +} +form { + border: none; + margin: 0; +} +textarea { + width: 100%; + padding: .1em; +} +select { + vertical-align: top; +} +/* Table of Contents */ +#toc, +.toc, +.mw-warning { + border: 1px solid #aaa; + background-color: #f9f9f9; + padding: 5px; + font-size: 95%; +} +#toc h2, +.toc h2 { + display: inline; + border: none; + padding: 0; + font-size: 100%; + font-weight: bold; +} +#toc #toctitle, +.toc #toctitle, +#toc .toctitle, +.toc .toctitle { + text-align: center; +} +#toc ul, +.toc ul { + list-style-type: none; + list-style-image: none; + margin-left: 0; + padding-left: 0; + text-align: left; +} +#toc ul ul, +.toc ul ul { + margin: 0 0 0 2em; +} +#toc .toctoggle, +.toc .toctoggle { + font-size: 94%; +} +/* Images */ +div.floatright, table.floatright { + clear: right; + float: right; + position: relative; + margin: 0 0 .5em .5em; + border: 0; +} +div.floatright p { font-style: italic; } +div.floatleft, table.floatleft { + float: left; + clear: left; + position: relative; + margin: 0 .5em .5em 0; + border: 0; +} +div.floatleft p { font-style: italic; } +/* Thumbnails */ +div.thumb { + margin-bottom: .5em; + border-style: solid; + border-color: white; + width: auto; + background-color: transparent; +} +div.thumbinner { + border: 1px solid #ccc; + padding: 3px !important; + background-color: #f9f9f9; + font-size: 94%; + text-align: center; + overflow: hidden; +} +html .thumbimage { + border: 1px solid #ccc; +} +html .thumbcaption { + border: none; + text-align: left; + line-height: 1.4em; + padding: 3px !important; + font-size: 94%; +} +div.magnify { + float: right; + border: none !important; + background: none !important; +} +div.magnify a, div.magnify img { + display: block; + border: none !important; + background: none !important; +} +div.tright { + clear: right; + float: right; + border-width: .5em 0 .8em 1.4em; +} +div.tleft { + float: left; + clear: left; + margin-right: .5em; + border-width: .5em 1.4em .8em 0; +} +img.thumbborder { + border: 1px solid #dddddd; +} +.hiddenStructure { + display: none; +} +/* Warning */ +.mw-warning { + margin-left: 50px; + margin-right: 50px; + text-align: center; +} +/* User Message */ +.usermessage { + background-color: #ffce7b; + border: 1px solid #ffa500; + color: black; + font-weight: bold; + margin: 2em 0 1em; + padding: .5em 1em; + vertical-align: middle; +} +/* Site Notice */ +#siteNotice { + text-align: center; + font-size: 0.8em; + margin: 0; +} + #siteNotice div, + #siteNotice p { + margin: 0; + padding: 0; + margin-bottom: 0.9em; + } +/* Categories */ +.catlinks { + border: 1px solid #aaa; + background-color: #f9f9f9; + padding: 5px; + margin-top: 1em; + clear: both; +} +/* Sub-navigation */ +#siteSub { + display: none; +} +#jump-to-nav { + display: none; +} +#contentSub, #contentSub2 { + font-size: 84%; + line-height: 1.2em; + margin: 0 0 1.4em 1em; + color: #7d7d7d; + width: auto; +} +span.subpages { + display: block; +} +/* Emulate Center */ +.center { + width: 100%; + text-align: center; +} +*.center * { + margin-left: auto; + margin-right: auto; +} +/* Small for tables and similar */ +.small, .small * { + font-size: 94%; +} +table.small { + font-size: 100%; +} +/* Edge Cases for Content */ +h1, h2 { + margin-bottom: .6em; +} +h3, h4, h5 { + margin-bottom: .3em; +} +#firstHeading { + padding-top: 0; + margin-top: 0; + padding-top: 0; + margin-bottom: 0.1em; + line-height: 1.2em; + font-size: 1.6em; + padding-bottom: 0; +} +#content a.external, +#content a[href ^="gopher://"] { + background: url(images/external-link-ltr-icon.png) center right no-repeat; + padding: 0 13px 0 0; +} +#content a[href ^="https://"], +.link-https { + background: url(images/lock-icon.png) center right no-repeat; + padding: 0 18px 0 0; +} +#content a[href ^="mailto:"], +.link-mailto { + background: url(images/mail-icon.png) center right no-repeat; + padding: 0 18px 0 0; +} +#content a[href ^="news://"] { + background: url(images/news-icon.png) center right no-repeat; + padding: 0 18px 0 0; +} +#content a[href ^="ftp://"], +.link-ftp { + background: url(images/file-icon.png) center right no-repeat; + padding: 0 18px 0 0; +} +#content a[href ^="irc://"], +#content a.extiw[href ^="irc://"], +.link-irc { + background: url(images/talk-icon.png) center right no-repeat; + padding: 0 18px 0 0; +} +#content a.external[href $=".ogg"], #content a.external[href $=".OGG"], +#content a.external[href $=".mid"], #content a.external[href $=".MID"], +#content a.external[href $=".midi"], #content a.external[href $=".MIDI"], +#content a.external[href $=".mp3"], #content a.external[href $=".MP3"], +#content a.external[href $=".wav"], #content a.external[href $=".WAV"], +#content a.external[href $=".wma"], #content a.external[href $=".WMA"], +.link-audio { + background: url("images/audio-icon.png") center right no-repeat; + padding: 0 18px 0 0; +} +#content a.external[href $=".ogm"], #content a.external[href $=".OGM"], +#content a.external[href $=".avi"], #content a.external[href $=".AVI"], +#content a.external[href $=".mpeg"], #content a.external[href $=".MPEG"], +#content a.external[href $=".mpg"], #content a.external[href $=".MPG"], +.link-video { + background: url("images/video-icon.png") center right no-repeat; + padding: 0 18px 0 0; +} +#content a.external[href $=".pdf"], #content a.external[href $=".PDF"], +#content a.external[href *=".pdf#"], #content a.external[href *=".PDF#"], +#content a.external[href *=".pdf?"], #content a.external[href *=".PDF?"], +.link-document { + background: url("images/document-icon.png") center right no-repeat; + padding: 0 18px 0 0; +} +/* Interwiki Styling (Disabled) */ +#content a.extiw, +#content a.extiw:active { + color: #36b; + background: none; + padding: 0; +} +#content a.external { + color: #36b; +} +#content .printfooter { + display: none; +} +/* Icon for Usernames */ +#pt-userpage, +#pt-anonuserpage, +#pt-login { + background: url(images/user-icon.png) left top no-repeat; + padding-left: 15px !important; + text-transform: none; +} + +.toccolours { + border: 1px solid #aaa; + background-color: #f9f9f9; + padding: 5px; + font-size: 95%; +} +#bodyContent { + position: relative; + width: 100%; +} +#mw-js-message { + font-size: 0.8em; +} +div#bodyContent { + line-height: 1.5em; +} + +/* Watch/Unwatch Icon Styling */ +#ca-unwatch.icon, +#ca-watch.icon { + margin-right:1px; +} +#ca-unwatch.icon a, +#ca-watch.icon a { + margin: 0; + padding: 0; + outline: none; + display: block; + width: 26px; + height: 2.5em; +} +#ca-unwatch.icon a { + background-image: url(images/watch-icons.png); + background-position: -43px 60%; +} +#ca-watch.icon a { + background-image: url(images/watch-icons.png); + background-position: 5px 60%; +} +#ca-unwatch.icon a:hover { + background-image: url(images/watch-icons.png); + background-position: -67px 60%; +} +#ca-watch.icon a:hover { + background-image: url(images/watch-icons.png); + background-position: -19px 60%; +} +#ca-unwatch.icon a.loading, +#ca-watch.icon a.loading { + background-image: url(images/watch-icon-loading.gif); + background-position: center 60%; +} +#ca-unwatch.icon a span, +#ca-watch.icon a span { + display: none; +} +div.vectorTabs ul { + background-image:url(images/tab-break.png); + background-position:right bottom; + background-repeat:no-repeat; +} diff --git a/skins/vector/main-rtl.css b/skins/vector/main-rtl.css new file mode 100644 index 00000000..5387289f --- /dev/null +++ b/skins/vector/main-rtl.css @@ -0,0 +1,1128 @@ +/* + * main-rtl.css is automatically generated using CSSJanus, a python script for + * creating RTL versions of otherwise LTR stylesheets. + * + * You may download the tool to rebuild this stylesheet + * http://code.google.com/p/cssjanus/ + * + * An online version of this tool can be used at: + * http://cssjanus.commoner.com/ + * + * The following command is used to generate the RTL version of this file + * ./cssjanus.py --swap_ltr_rtl_in_url < main-ltr.css > main-rtl.css + * + * Any rules which should not be flipped should be prepended with @noflip in + * a comment block. + */ +/* Framework */ +html, +body { + height: 100%; + margin: 0; + padding: 0; + font-family: sans-serif; + font-size: 1em; +} +body { + background-color: #f3f3f3; + background-image: url(images/page-base.png); +} +/* Content */ +#content { + margin-right: 10em; + padding: 1em; + background-image: url(images/border.png); + background-position: top right; + background-repeat: repeat-y; + background-color: white; +} +/* Head */ +#mw-page-base { + height: 5em; + background-color: white; + background-image: url(images/page-fade.png); + background-position: bottom right; + background-repeat: repeat-x; +} +#mw-head-base { + margin-top: -5em; + margin-right: 10em; + height: 5em; + background-image: url(images/border.png); + background-position: bottom right; + background-repeat: repeat-x; +} +#mw-head { + position: absolute; + top: 0; + left: 0; + width: 100%; +} +#mw-head h5 { + margin: 0; + padding: 0; +} + /* Hide empty portlets */ + div.emptyPortlet { + display: none; + } + /* Personal */ + #p-personal { + position: absolute; + top: 0; + margin-right: 10em; + left: 0.75em; + } + #p-personal h5 { + display: none; + } + #p-personal ul { + list-style: none; + margin: 0; + padding: 0; + } + /* @noflip */ + #p-personal li { + line-height: 1.125em; + float: left; + } + #p-personal li { + margin-right: 0.75em; + margin-top: 0.5em; + font-size: 0.75em; + } + /* Navigation Containers */ + #left-navigation { + position: absolute; + right: 10em; + top: 2.5em; + } + #right-navigation { + float: left; + margin-top: 2.5em; + } + /* Navigation Labels */ + div.vectorTabs h5, + div.vectorMenu h5 span { + display: none; + } + /* Namespaces and Views */ + /* @noflip */ + div.vectorTabs { + float: left; + } + div.vectorTabs { + background-image: url(images/tab-break.png); + background-position: bottom right; + background-repeat: no-repeat; + padding-right: 1px; + } + /* @noflip */ + div.vectorTabs ul { + float: left; + } + div.vectorTabs ul { + height: 100%; + list-style: none; + margin: 0; + padding: 0; + } + /* @noflip */ + div.vectorTabs ul li { + float: left; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorTabs ul li { + line-height: 1.125em; + display: inline-block; + height: 100%; + margin: 0; + padding: 0; + background-color: #f3f3f3; + background-image: url(images/tab-normal-fade.png); + background-position: bottom right; + background-repeat: repeat-x; + white-space:nowrap; + } + /* IGNORED BY IE6 */ + div.vectorTabs ul > li { + display: block; + } + div.vectorTabs li.selected { + background-image: url(images/tab-current-fade.png); + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorTabs li a { + display: inline-block; + height: 2.5em; + padding-right: 0.4em; + padding-left: 0.4em; + background-image: url(images/tab-break.png); + background-position: bottom left; + background-repeat: no-repeat; + } + div.vectorTabs li a, + div.vectorTabs li a span { + color: #0645ad; + cursor: pointer; + } + div.vectorTabs li a span { + font-size: 0.8em; + } + /* IGNORED BY IE6 */ + div.vectorTabs li > a { + display: block; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorTabs a span { + display: inline-block; + padding-top: 1.25em; + } + /* IGNORED BY IE6 */ + /* @noflip */ + div.vectorTabs a > span { + float: left; + display: block; + } + div.vectorTabs li.selected a, + div.vectorTabs li.selected a span, + div.vectorTabs li.selected a:visited + div.vectorTabs li.selected a:visited span { + color: #333333; + text-decoration: none; + } + div.vectorTabs li.new a, + div.vectorTabs li.new a span, + div.vectorTabs li.new a:visited, + div.vectorTabs li.new a:visited span { + color: #a55858; + } + /* Variants and Actions */ + /* @noflip */ + div.vectorMenu { + direction: ltr; + float: left; + background-image: url(images/arrow-down-icon.png); + background-position: center center; + background-repeat: no-repeat; + } + /* @noflip */ + body.rtl div.vectorMenu { + direction: rtl; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + /* @noflip */ + #mw-head div.vectorMenu h5 { + float: left; + background-image: url(images/tab-break.png); + background-repeat: no-repeat; + } + /* IGNORED BY IE6 */ + #mw-head div.vectorMenu > h5 { + background-image: none; + } + #mw-head div.vectorMenu h5 { + background-position: bottom right; + margin-right: -1px; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + /* @noflip */ + div.vectorMenu h5 a { + display: inline-block; + width: 24px; + height: 2.5em; + text-decoration: none; + background-image: url(images/tab-break.png); + background-repeat: no-repeat; + } + div.vectorMenu h5 a{ + background-position: bottom left; + } + /* IGNORED BY IE6 */ + div.vectorMenu h5 > a { + display: block; + } + div.vectorMenu div.menu { + position: relative; + display: none; + clear: both; + text-align: right; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + /* @noflip */ + body.rtl div.vectorMenu div.menu { + margin-left: 24px; + } + /* IGNORED BY IE6 */ + /* @noflip */ + body.rtl div.vectorMenu > div.menu { + margin-left: auto; + } + /* Fixes old versions of FireFox */ + /* @noflip */ + body.rtl div.vectorMenu > div.menu, + x:-moz-any-link { + margin-left: 23px; + } + div.vectorMenu:hover div.menu { + display: block; + } + div.vectorMenu ul { + position: absolute; + background-color: white; + border: solid 1px silver; + border-top-width: 0; + list-style: none; + list-style-image: none; + list-style-type: none; + padding: 0; + margin: 0; + margin-right: -1px; + text-align: right; + } + /* Fixes old versions of FireFox */ + div.vectorMenu ul, + x:-moz-any-link { + min-width: 5em; + } + /* Returns things back to normal in modern versions of FireFox */ + div.vectorMenu ul, + x:-moz-any-link, + x:default { + min-width: 0; + } + div.vectorMenu li { + padding: 0; + margin: 0; + text-align: right; + line-height: 1em; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + div.vectorMenu li a { + display: inline-block; + padding: 0.5em; + white-space: nowrap; + } + /* IGNORED BY IE6 */ + div.vectorMenu li > a { + display: block; + } + div.vectorMenu li a { + color: #0645ad; + cursor: pointer; + font-size: 0.8em; + } + div.vectorMenu li.selected a, + div.vectorMenu li.selected a:visited { + color: #333333; + text-decoration: none; + } + /* Search */ + #p-search h5 { + display: none; + } + /* @noflip */ + #p-search { + float: left; + } + #p-search { + margin-left: 0.5em; + margin-right: 0.5em; + } + #p-search form, + #p-search input { + margin: 0; + margin-top: 0.4em; + } + #simpleSearch { + margin-top: 0.5em; + position: relative; + border: solid 1px #AAAAAA; + background-color: white; + background-image: url(images/search-fade.png); + background-position: top right; + background-repeat: repeat-x; + } + #simpleSearch label { + font-size: 0.8em; + top: 0.25em; + } + #simpleSearch input#searchInput { + margin: 0; + border-width: 0; + padding: 0.25em; + line-height: 1em; + font-size: 0.8em; + width: 9em; + background-color: transparent; + } + /* OVERRIDDEN BY COMPLIANT BROWSERS */ + #simpleSearch button#searchButton { + margin: 0; + padding: 0; + width: 1.75em; + height: 1.5em; + border: none; + cursor: pointer; + background-color: transparent; + background-image: url(images/search-rtl.png); + background-position: center center; + background-repeat: no-repeat; + } + /* IGNORED BY IE6 */ + #simpleSearch > button#searchButton { + height: 100%; + } + .suggestions-special .special-label { + font-size: 0.8em; + color: gray; + } + .suggestions-special .special-query { + color: black; + font-style: italic; + } + .suggestions-special .special-hover { + background-color: silver; + } +/* Panel */ +#mw-panel { + position: absolute; + top: 160px; + padding-top: 1em; + width: 10em; + right: 0; +} + #mw-panel div.portal { + padding-bottom: 1.5em; + } + #mw-panel div.portal h5 { + font-weight: normal; + color: #444444; + padding: 0.25em; + padding-top: 0; + padding-right: 1.75em; + cursor: default; + border: none; + font-size: 0.75em; + } + #mw-panel div.portal div.body { + margin: 0; + padding-top: 0.5em; + margin-right: 1.25em; + background-image: url(images/portal-break.png); + background-repeat: no-repeat; + background-position: top right; + } + #mw-panel div.portal div.body ul { + list-style: none; + list-style-image: none; + list-style-type: none; + padding: 0; + margin: 0; + } + #mw-panel div.portal div.body ul li { + line-height: 1.125em; + padding: 0; + padding-bottom: 0.5em; + margin: 0; + overflow: hidden; + font-size: 0.75em; + } + #mw-panel div.portal div.body ul li a { + color: #0645ad; + } + #mw-panel div.portal div.body ul li a:visited { + color: #0b0080; + } +/* Footer */ +#footer { + margin-right: 10em; + margin-top: 0; + padding: 0.75em; + background-image: url(images/border.png); + background-position: top right; + background-repeat: repeat-x; +} +#footer ul { + list-style: none; + list-style-image: none; + list-style-type: none; + margin: 0; + padding: 0; +} +#footer ul li { + margin: 0; + padding: 0; + padding-top: 0.5em; + padding-bottom: 0.5em; + color: #333333; + font-size: 0.7em; +} +#footer #footer-icons { + float: left; +} +/* @noflip */ +body.ltr #footer #footer-places { + float: left; +} +#footer #footer-info li { + line-height: 1.4em; +} +#footer #footer-icons li { + float: right; + margin-right: 0.5em; + line-height: 2em; +} +#footer #footer-places li { + float: right; + margin-left: 1em; + line-height: 2em; +} +/* Logo */ +#p-logo { + position: absolute; + top: -160px; + right: 0; + width: 10em; + height: 160px; +} +#p-logo a { + display: block; + width: 10em; + height: 160px; + background-repeat: no-repeat; + background-position: center center; + text-decoration: none; +} + +/* + * + * The following code is highly modified from monobook. It would be nice if the + * preftoc id was more human readable like preferences-toc for instance, + * howerver this would require backporting the other skins. + */ + +/* Preferences */ +#preftoc { + /* Tabs */ + width: 100%; + float: right; + clear: both; + margin: 0 !important; + padding: 0 !important; + background-image: url(images/preferences-break.png); + background-position: bottom right; + background-repeat: no-repeat; +} + #preftoc li { + /* Tab */ + float: right; + margin: 0; + padding: 0; + padding-left: 1px; + height: 2.25em; + white-space: nowrap; + list-style-type: none; + list-style-image: none; + background-image: url(images/preferences-break.png); + background-position: bottom left; + background-repeat: no-repeat; + } + /* IGNORED BY IE6 */ + #preftoc li:first-child { + margin-right: 1px; + } + #preftoc a, + #preftoc a:active { + display: inline-block; + position: relative; + color: #0645ad; + padding: 0.5em; + text-decoration: none; + background-image: none; + font-size: 0.9em; + } + #preftoc a:hover { + text-decoration: underline; + } + #preftoc li.selected a { + background-image: url(images/preferences-fade.png); + background-position: bottom; + background-repeat: repeat-x; + color: #333333; + text-decoration: none; + } +#preferences { + float: right; + width: 100%; + margin: 0; + margin-top: -2px; + clear: both; + border: solid 1px #cccccc; + background-color: #f9f9f9; + background-image: url(images/preferences-base.png); +} +#preferences fieldset.prefsection { + border: none; + padding: 0; + margin: 1em; +} +#preferences fieldset.prefsection fieldset { + border: none; + border-top: solid 1px #cccccc; +} +#preferences legend { + color: #666666; +} +#preferences fieldset.prefsection legend.mainLegend { + display: none; +} +#preferences td { + padding-right: 0.5em; + padding-left: 0.5em; +} +#preferences td.htmlform-tip { + font-size: x-small; + padding: .2em 2em; + color: #666666; +} +#preferences div.mw-prefs-buttons { + padding: 1em; +} +#preferences div.mw-prefs-buttons input { + margin-left: 0.25em; +} + +/* + * Styles for the user login and create account forms + */ +#userlogin, #userloginForm { + border: solid 1px #cccccc; + padding: 1.2em; + margin: .5em; + float: right; +} + +#userlogin { + min-width: 20em; + max-width: 90%; + width: 40em; +} + +/* + * + * The following code is slightly modified from monobook + * + */ +#content { + line-height: 1.5em; +} +#bodyContent { + font-size: 0.8em; +} +/* Links */ +a { + text-decoration: none; + color: #0645ad; + background: none; +} +a:visited { + color: #0b0080; +} +a:active { + color: #faa700; +} +a:hover { + text-decoration: underline; +} +a.stub { + color: #772233; +} +a.new, #p-personal a.new { + color: #ba0000; +} +a.new:visited, #p-personal a.new:visited { + color: #a55858; +} + +/* Inline Elements */ +img { + border: none; + vertical-align: middle; +} +hr { + height: 1px; + color: #aaa; + background-color: #aaa; + border: 0; + margin: .2em 0 .2em 0; +} + +/* Structural Elements */ +h1, +h2, +h3, +h4, +h5, +h6 { + color: black; + background: none; + font-weight: normal; + margin: 0; + padding-top: .5em; + padding-bottom: .17em; + border-bottom: 1px solid #aaa; + width: auto; +} +h1 { font-size: 188%; } +h1 .editsection { font-size: 53%; } +h2 { font-size: 150%; } +h2 .editsection { font-size: 67%; } +h3, +h4, +h5, +h6 { + border-bottom: none; + font-weight: bold; +} +h3 { font-size: 132%; } +h3 .editsection { font-size: 76%; font-weight: normal; } +h4 { font-size: 116%; } +h4 .editsection { font-size: 86%; font-weight: normal; } +h5 { font-size: 100%; } +h5 .editsection { font-weight: normal; } +h6 { font-size: 80%; } +h6 .editsection { font-size: 125%; font-weight: normal; } +p { + margin: .4em 0 .5em 0; + line-height: 1.5em; +} + p img { + margin: 0; + } +abbr, +acronym, +.explain { + border-bottom: 1px dotted black; + color: black; + background: none; + cursor: help; +} +q { + font-family: Times, "Times New Roman", serif; + font-style: italic; +} +/* Disabled for now +blockquote { + font-family: Times, "Times New Roman", serif; + font-style: italic; +}*/ +code { + background-color: #f9f9f9; +} +pre { + padding: 1em; + border: 1px dashed #2f6fab; + color: black; + background-color: #f9f9f9; + line-height: 1.1em; +} +ul { + line-height: 1.5em; + list-style-type: square; + margin: .3em 1.5em 0 0; + padding: 0; + list-style-image: url(images/bullet-icon.png); +} +ol { + line-height: 1.5em; + margin: .3em 3.2em 0 0; + padding: 0; + list-style-image: none; +} +li { + margin-bottom: .1em; +} +dt { + font-weight: bold; + margin-bottom: .1em; +} +dl { + margin-top: .2em; + margin-bottom: .5em; +} +dd { + line-height: 1.5em; + margin-right: 2em; + margin-bottom: .1em; +} +/* Tables */ +table { + font-size: 100%; + color: black; + /* we don't want the bottom borders of

    s to be visible through + * floated tables */ + background-color: white; +} +fieldset table { + /* but keep table layouts in forms clean... */ + background: none; +} +/* Forms */ +fieldset { + border: 1px solid #2f6fab; + margin: 1em 0 1em 0; + padding: 0 1em 1em; + line-height: 1.5em; +} + fieldset.nested { + margin: 0 0 0.5em 0; + padding: 0 0.5em 0.5em; + } +legend { + padding: .5em; + font-size: 95%; +} +form { + border: none; + margin: 0; +} +textarea { + width: 100%; + padding: .1em; +} +select { + vertical-align: top; +} +/* Table of Contents */ +#toc, +.toc, +.mw-warning { + border: 1px solid #aaa; + background-color: #f9f9f9; + padding: 5px; + font-size: 95%; +} +#toc h2, +.toc h2 { + display: inline; + border: none; + padding: 0; + font-size: 100%; + font-weight: bold; +} +#toc #toctitle, +.toc #toctitle, +#toc .toctitle, +.toc .toctitle { + text-align: center; +} +#toc ul, +.toc ul { + list-style-type: none; + list-style-image: none; + margin-right: 0; + padding-right: 0; + text-align: right; +} +#toc ul ul, +.toc ul ul { + margin: 0 2em 0 0; +} +#toc .toctoggle, +.toc .toctoggle { + font-size: 94%; +} +/* Images */ +div.floatright, table.floatright { + clear: left; + float: left; + position: relative; + margin: 0 .5em .5em 0; + border: 0; +} +div.floatright p { font-style: italic; } +div.floatleft, table.floatleft { + float: right; + clear: right; + position: relative; + margin: 0 0 .5em .5em; + border: 0; +} +div.floatleft p { font-style: italic; } +/* Thumbnails */ +div.thumb { + margin-bottom: .5em; + border-style: solid; + border-color: white; + width: auto; + background-color: transparent; +} +div.thumbinner { + border: 1px solid #ccc; + padding: 3px !important; + background-color: #f9f9f9; + font-size: 94%; + text-align: center; + overflow: hidden; +} +html .thumbimage { + border: 1px solid #ccc; +} +html .thumbcaption { + border: none; + text-align: right; + line-height: 1.4em; + padding: 3px !important; + font-size: 94%; +} +div.magnify { + float: left; + border: none !important; + background: none !important; +} +div.magnify a, div.magnify img { + display: block; + border: none !important; + background: none !important; +} +div.tright { + clear: left; + float: left; + border-width: .5em 1.4em .8em 0; +} +div.tleft { + float: right; + clear: right; + margin-left: .5em; + border-width: .5em 0 .8em 1.4em; +} +img.thumbborder { + border: 1px solid #dddddd; +} +.hiddenStructure { + display: none; +} +/* Warning */ +.mw-warning { + margin-right: 50px; + margin-left: 50px; + text-align: center; +} +/* User Message */ +.usermessage { + background-color: #ffce7b; + border: 1px solid #ffa500; + color: black; + font-weight: bold; + margin: 2em 0 1em; + padding: .5em 1em; + vertical-align: middle; +} +/* Site Notice */ +#siteNotice { + text-align: center; + font-size: 0.8em; + margin: 0; +} + #siteNotice div, + #siteNotice p { + margin: 0; + padding: 0; + margin-bottom: 0.9em; + } +/* Categories */ +.catlinks { + border: 1px solid #aaa; + background-color: #f9f9f9; + padding: 5px; + margin-top: 1em; + clear: both; +} +/* Sub-navigation */ +#siteSub { + display: none; +} +#jump-to-nav { + display: none; +} +#contentSub, #contentSub2 { + font-size: 84%; + line-height: 1.2em; + margin: 0 1em 1.4em 0; + color: #7d7d7d; + width: auto; +} +span.subpages { + display: block; +} +/* Emulate Center */ +.center { + width: 100%; + text-align: center; +} +*.center * { + margin-right: auto; + margin-left: auto; +} +/* Small for tables and similar */ +.small, .small * { + font-size: 94%; +} +table.small { + font-size: 100%; +} +/* Edge Cases for Content */ +h1, h2 { + margin-bottom: .6em; +} +h3, h4, h5 { + margin-bottom: .3em; +} +#firstHeading { + padding-top: 0; + margin-top: 0; + padding-top: 0; + margin-bottom: 0.1em; + line-height: 1.2em; + font-size: 1.6em; + padding-bottom: 0; +} +#content a.external, +#content a[href ^="gopher://"] { + background: url(images/external-link-rtl-icon.png) center left no-repeat; + padding: 0 0 0 13px; +} +#content a[href ^="https://"], +.link-https { + background: url(images/lock-icon.png) center left no-repeat; + padding: 0 0 0 18px; +} +#content a[href ^="mailto:"], +.link-mailto { + background: url(images/mail-icon.png) center left no-repeat; + padding: 0 0 0 18px; +} +#content a[href ^="news://"] { + background: url(images/news-icon.png) center left no-repeat; + padding: 0 0 0 18px; +} +#content a[href ^="ftp://"], +.link-ftp { + background: url(images/file-icon.png) center left no-repeat; + padding: 0 0 0 18px; +} +#content a[href ^="irc://"], +#content a.extiw[href ^="irc://"], +.link-irc { + background: url(images/talk-icon.png) center left no-repeat; + padding: 0 0 0 18px; +} +#content a.external[href $=".ogg"], #content a.external[href $=".OGG"], +#content a.external[href $=".mid"], #content a.external[href $=".MID"], +#content a.external[href $=".midi"], #content a.external[href $=".MIDI"], +#content a.external[href $=".mp3"], #content a.external[href $=".MP3"], +#content a.external[href $=".wav"], #content a.external[href $=".WAV"], +#content a.external[href $=".wma"], #content a.external[href $=".WMA"], +.link-audio { + background: url("images/audio-icon.png") center left no-repeat; + padding: 0 0 0 18px; +} +#content a.external[href $=".ogm"], #content a.external[href $=".OGM"], +#content a.external[href $=".avi"], #content a.external[href $=".AVI"], +#content a.external[href $=".mpeg"], #content a.external[href $=".MPEG"], +#content a.external[href $=".mpg"], #content a.external[href $=".MPG"], +.link-video { + background: url("images/video-icon.png") center left no-repeat; + padding: 0 0 0 18px; +} +#content a.external[href $=".pdf"], #content a.external[href $=".PDF"], +#content a.external[href *=".pdf#"], #content a.external[href *=".PDF#"], +#content a.external[href *=".pdf?"], #content a.external[href *=".PDF?"], +.link-document { + background: url("images/document-icon.png") center left no-repeat; + padding: 0 0 0 18px; +} +/* Interwiki Styling (Disabled) */ +#content a.extiw, +#content a.extiw:active { + color: #36b; + background: none; + padding: 0; +} +#content a.external { + color: #36b; +} +#content .printfooter { + display: none; +} +/* Icon for Usernames */ +#pt-userpage, +#pt-anonuserpage, +#pt-login { + background: url(images/user-icon.png) right top no-repeat; + padding-right: 15px !important; + text-transform: none; +} + +.toccolours { + border: 1px solid #aaa; + background-color: #f9f9f9; + padding: 5px; + font-size: 95%; +} +#bodyContent { + position: relative; + width: 100%; +} +#mw-js-message { + font-size: 0.8em; +} +div#bodyContent { + line-height: 1.5em; +} + +/* Watch/Unwatch Icon Styling */ +#ca-unwatch.icon, +#ca-watch.icon { + margin-left:1px; +} +#ca-unwatch.icon a, +#ca-watch.icon a { + margin: 0; + padding: 0; + outline: none; + display: block; + width: 26px; + height: 2.5em; +} +#ca-unwatch.icon a { + background-image: url(images/watch-icons.png); + background-position: -43px 60%; +} +#ca-watch.icon a { + background-image: url(images/watch-icons.png); + background-position: 5px 60%; +} +#ca-unwatch.icon a:hover { + background-image: url(images/watch-icons.png); + background-position: -67px 60%; +} +#ca-watch.icon a:hover { + background-image: url(images/watch-icons.png); + background-position: -19px 60%; +} +#ca-unwatch.icon a.loading, +#ca-watch.icon a.loading { + background-image: url(images/watch-icon-loading.gif); + background-position: center 60%; +} +#ca-unwatch.icon a span, +#ca-watch.icon a span { + display: none; +} +div.vectorTabs ul { + background-image:url(images/tab-break.png); + background-position:left bottom; + background-repeat:no-repeat; +} diff --git a/skins/vector/wiki-indexed.png b/skins/vector/wiki-indexed.png new file mode 100644 index 00000000..189a2ae3 Binary files /dev/null and b/skins/vector/wiki-indexed.png differ diff --git a/skins/vector/wiki.png b/skins/vector/wiki.png new file mode 100644 index 00000000..2463b521 Binary files /dev/null and b/skins/vector/wiki.png differ diff --git a/t/.htaccess b/t/.htaccess deleted file mode 100644 index 3a428827..00000000 --- a/t/.htaccess +++ /dev/null @@ -1 +0,0 @@ -Deny from all diff --git a/t/00-test.t b/t/00-test.t deleted file mode 100644 index b9ed2038..00000000 --- a/t/00-test.t +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env php - diff --git a/t/README b/t/README deleted file mode 100644 index b3b9f420..00000000 --- a/t/README +++ /dev/null @@ -1,52 +0,0 @@ -=head1 NAME - -F - MediaWiki test tree - -=head1 DESCRIPTION - -This is the MediaWiki test tree (well, one of them), tests in this -directory are self-contained programs that produce TAP output via the -F module (/trunk/phase3/t/Test.php) (see -http://search.cpan.org/~petdance/TAP-1.00/TAP.pm#THE_TAP_FORMAT for -information on the TAP format). - -=head1 Running the tests - -To run all tests, you can run - - make test - -Since the tests are self-contained PHP programs you can run them -(Xml.t here) as: - - php t/inc/Xml.t - -That'll give you the raw TAP output, but what you probably want is to -use a TAP formatter such as L: - - prove t/inc/Xml.t # add -v for the verbose version - -or to run all the tests: - - prove -r t - -=head1 TODO - -=over - -=item * - -Rewrite the rest of the F stuff to use L and move it -here - -=item * - -Make the parsertests use TAP? - -=item * - -Write unit tests for the entire codebase:) - -=back - -=cut diff --git a/t/Search.inc b/t/Search.inc deleted file mode 100644 index 25229306..00000000 --- a/t/Search.inc +++ /dev/null @@ -1,161 +0,0 @@ -isOpen() ) { - if ( !( stristr( $db->getSoftwareLink(), 'MySQL') && version_compare( $db->getServerVersion(), '4.1', '<' ) ) ) { - # Database that supports CREATE TABLE ... LIKE - foreach ($tables as $tbl) { - $newTableName = $db->tableName( $tbl ); - $tableName = $oldPrefix . $tbl; - $db->query("CREATE TEMPORARY TABLE $newTableName (LIKE $tableName)"); - } - } else { - # 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.... - foreach ($tables as $tbl) { - $res = $db->query("SHOW CREATE TABLE $tbl"); - $row = $db->fetchRow($res); - $create = $row[1]; - $create_tmp = preg_replace('/CREATE TABLE `(.*?)`/', 'CREATE TEMPORARY TABLE `' - . $wgDBprefix . '\\1`', $create); - if ($create === $create_tmp) { - # Couldn't do replacement - wfDie( "could not create temporary table $tbl" ); - } - $db->query($create_tmp); - } - - } - return $db; - } else { - // Something amiss - return null; - } -} - -class SearchEngineTest { - var $db, $search; - - function __construct( SearchEngine $search ){ - $this->search = $search; - $this->db = $this->search->db; - } - - function insertSearchData() { - $this->db->safeQuery( <<db->tableName( 'page' ) ); - $this->db->safeQuery( <<db->tableName( 'revision' ) ); - $this->db->safeQuery( <<db->tableName( 'text' ) ); - $this->db->safeQuery( <<db->tableName( 'searchindex' ) ); - } - - function fetchIds( $results ) { - $matches = array(); - while( $row = $results->next() ) { - $matches[] = $row->getTitle()->getPrefixedText(); - } - $results->free(); - # Search is not guaranteed to return results in a certain order; - # sort them numerically so we will compare simply that we received - # the expected matches. - sort( $matches ); - return $matches; - } - - function run(){ - if( is_null( $this->db ) ){ - fail( "Can't find a database to test with." ); - return; - } - - $this->insertSearchData(); - plan( 4 ); - - $exp = array( 'Smithee' ); - $got = $this->fetchIds( $this->search->searchText( 'smithee' ) ); - is( $got, $exp, "Plain search" ); - - $exp = array( 'Alan Smithee', 'Smithee' ); - $got = $this->fetchIds( $this->search->searchTitle( 'smithee' ) ); - is( $got, $exp, "Title search" ); - - $this->search->setNamespaces( array( 0, 1, 4 ) ); - - $exp = array( 'Smithee', 'Talk:Main Page', ); - $got = $this->fetchIds( $this->search->searchText( 'smithee' ) ); - is( $got, $exp, "Power search" ); - - $exp = array( 'Alan Smithee', 'Smithee', 'Talk:Smithee', ); - $got = $this->fetchIds( $this->search->searchTitle( 'smithee' ) ); - is( $got, $exp, "Title power search" ); - } -} diff --git a/t/Test.php b/t/Test.php deleted file mode 100644 index 7904f3ba..00000000 --- a/t/Test.php +++ /dev/null @@ -1,496 +0,0 @@ - null, - - # How many tests we've run, if 'planned' is still null by the time we're - # done we report the total count at the end - 'run' => 0, - - # Are are we currently within todo_start()/todo_end() ? - 'todo' => array(), -); - -function plan($plan, $why = '') -{ - global $__Test; - - $__Test['planned'] = true; - - switch ($plan) - { - case 'no_plan': - $__Test['planned'] = false; - break; - case 'skip_all'; - printf("1..0%s\n", $why ? " # Skip $why" : ''); - exit; - default: - printf("1..%d\n", $plan); - break; - } -} - -function pass($desc = '') -{ - return _proclaim(true, $desc); -} - -function fail($desc = '') -{ - return _proclaim(false, $desc); -} - -function ok($cond, $desc = '') { - return _proclaim($cond, $desc); -} - -function is($got, $expected, $desc = '') { - $pass = $got == $expected; - return _proclaim($pass, $desc, /* todo */ false, $got, $expected); -} - -function isnt($got, $expected, $desc = '') { - $pass = $got != $expected; - return _proclaim($pass, $desc, /* todo */ false, $got, $expected, /* negated */ true); -} - -function like($got, $expected, $desc = '') { - $pass = preg_match($expected, $got); - return _proclaim($pass, $desc, /* todo */ false, $got, $expected); -} - -function unlike($got, $expected, $desc = '') { - $pass = !preg_match($expected, $got); - return _proclaim($pass, $desc, /* todo */ false, $got, $expected, /* negated */ true); -} - -function cmp_ok($got, $op, $expected, $desc = '') -{ - $pass = null; - - # See http://www.php.net/manual/en/language.operators.comparison.php - switch ($op) - { - case '==': - $pass = $got == $expected; - break; - case '===': - $pass = $got === $expected; - break; - case '!=': - case '<>': - $pass = $got != $expected; - break; - case '!==': - $pass = $got !== $expected; - break; - case '<': - $pass = $got < $expected; - break; - case '>': - $pass = $got > $expected; - break; - case '<=': - $pass = $got <= $expected; - break; - case '>=': - $pass = $got >= $expected; - break; - default: - if (function_exists($op)) { - $pass = $op($got, $expected); - } else { - die("No such operator or function $op\n"); - } - } - - return _proclaim($pass, $desc, /* todo */ false, $got, "$got $op $expected"); -} - -function diag($message) -{ - if (is_array($message)) - { - $message = implode("\n", $message); - } - - foreach (explode("\n", $message) as $line) - { - echo "# $line\n"; - } -} - -function include_ok($file, $desc = '') -{ - $pass = include $file; - return _proclaim($pass, $desc == '' ? "include $file" : $desc); -} - -function require_ok($file, $desc = '') -{ - $pass = require $file; - return _proclaim($pass, $desc == '' ? "require $file" : $desc); -} - -function is_deeply($got, $expected, $desc = '') -{ - $diff = _cmp_deeply($got, $expected); - $pass = is_null($diff); - - if (!$pass) { - $got = strlen($diff['gpath']) ? ($diff['gpath'] . ' = ' . $diff['got']) - : _repl($got); - $expected = strlen($diff['epath']) ? ($diff['epath'] . ' = ' . $diff['expected']) - : _repl($expected); - } - - _proclaim($pass, $desc, /* todo */ false, $got, $expected); -} - -function isa_ok($obj, $expected, $desc = '') -{ - $pass = is_a($obj, $expected); - _proclaim($pass, $desc, /* todo */ false, $name, $expected); -} - -function todo_start($why = '') -{ - global $__Test; - - $__Test['todo'][] = $why; -} - -function todo_end() -{ - global $__Test; - - if (count($__Test['todo']) == 0) { - die("todo_end() called without a matching todo_start() call"); - } else { - array_pop($__Test['todo']); - } -} - -# -# The code below consists of private utility functions for the above functions -# - -function _proclaim( - $cond, # bool - $desc = '', - $todo = false, - $got = null, - $expected = null, - $negate = false) { - - global $__Test; - - $__Test['run'] += 1; - - # We're in a TODO block via todo_start()/todo_end(). TODO via specific - # functions is currently unimplemented and will probably stay that way - if (count($__Test['todo'])) { - $todo = true; - } - - # Everything after the first # is special, so escape user-supplied messages - $desc = str_replace('#', '\\#', $desc); - $desc = str_replace("\n", '\\n', $desc); - - $ok = $cond ? "ok" : "not ok"; - $directive = ''; - - if ($todo) { - $todo_idx = count($__Test['todo']) - 1; - $directive .= ' # TODO ' . $__Test['todo'][$todo_idx]; - } - - printf("%s %d %s%s\n", $ok, $__Test['run'], $desc, $directive); - - # report a failure - if (!$cond) { - # Every public function in this file calls _proclaim so our culprit is - # the second item in the stack - $caller = debug_backtrace(); - $call = $caller['1']; - - diag( - sprintf(" Failed%stest '%s'\n in %s at line %d\n got: %s\n expected: %s", - $todo ? ' TODO ' : ' ', - $desc, - $call['file'], - $call['line'], - $got, - $expected - ) - ); - } - - return $cond; -} - -function _test_ends() -{ - global $__Test; - - if (count($__Test['todo']) != 0) { - $todos = join("', '", $__Test['todo']); - die("Missing todo_end() for '$todos'"); - } - - if (!$__Test['planned']) { - printf("1..%d\n", $__Test['run']); - } -} - -# -# All of the below is for is_deeply() -# - -function _repl($obj, $deep = true) { - if (is_string($obj)) { - return "'" . $obj . "'"; - } else if (is_numeric($obj)) { - return $obj; - } else if (is_null($obj)) { - return 'null'; - } else if (is_bool($obj)) { - return $obj ? 'true' : 'false'; - } else if (is_array($obj)) { - return _repl_array($obj, $deep); - }else { - return gettype($obj); - } -} - -function _diff($gpath, $got, $epath, $expected) { - return array( - 'gpath' => $gpath, - 'got' => $got, - 'epath' => $epath, - 'expected' => $expected - ); -} - -function _idx($obj, $path = '') { - return $path . '[' . _repl($obj) . ']'; -} - -function _cmp_deeply($got, $exp, $path = '') { - if (is_array($exp)) { - - if (!is_array($got)) { - return _diff($path, _repl($got), $path, _repl($exp)); - } - - $gk = array_keys($got); - $ek = array_keys($exp); - $mc = max(count($gk), count($ek)); - - for ($el = 0; $el < $mc; $el++) { - # One array shorter than the other? - if ($el >= count($ek)) { - return _diff(_idx($gk[$el], $path), _repl($got[$gk[$el]]), - 'missing', 'nothing'); - } else if ($el >= count($gk)) { - return _diff('missing', 'nothing', - _idx($ek[$el], $path), _repl($exp[$ek[$el]])); - } - - # Keys differ? - if ($gk[$el] != $ek[$el]) { - return _diff(_idx($gk[$el], $path), _repl($got[$gk[$el]]), - _idx($ek[$el], $path), _repl($exp[$ek[$el]])); - } - - # Recurse - $rc = _cmp_deeply($got[$gk[$el]], $exp[$ek[$el]], _idx($gk[$el], $path)); - if (!is_null($rc)) { - return $rc; - } - } - } - else { - # Default to serialize hack - if (serialize($got) != serialize($exp)) { - return _diff($path, _repl($got), $path, _repl($exp)); - } - } - - return null; -} - -function _plural($n, $singular, $plural = null) { - if (is_null($plural)) { - $plural = $singular . 's'; - } - return $n == 1 ? "$n $singular" : "$n $plural"; -} - -function _repl_array($obj, $deep) { - if ($deep) { - $slice = array_slice($obj, 0, 3); # Increase from 3 to show more - $repl = array(); - $next = 0; - foreach ($slice as $idx => $el) { - $elrep = _repl($el, false); - if (is_numeric($idx) && $next == $idx) { - // Numeric index - $next++; - } else { - // Out of sequence or non-numeric - $elrep = _repl($idx, false) . ' => ' . $elrep; - } - $repl[] = $elrep; - } - $more = count($obj) - count($slice); - if ($more > 0) { - $repl[] = '... ' . _plural($more, 'more element') . ' ...'; - } - return 'array(' . join(', ', $repl) . ')'; - } - else { - return 'array(' . count($obj) . ')'; - } -} - -/* - -=head1 NAME - -Test.php - TAP test framework for PHP with a L-like interface - -=head1 SYNOPSIS - - #!/usr/bin/env php - - -=head1 DESCRIPTION - -F is an implementation of Perl's L for PHP. Like -Test::More it produces language agnostic TAP output (see L) which -can then be gathered, formatted and summarized by a program that -understands TAP such as prove(1). - -=head1 HOWTO - -First place the F in the project root or somewhere else in -the include path where C and C will find it. - -Then make a place to put your tests in, it's customary to place TAP -tests in a directory named F under the root but they can be -anywhere you like. Make a test in this directory or one of its subdirs -and try running it with php(1): - - $ php t/pass.t - 1..1 - ok 1 This dummy test passed - -The TAP output consists of very simple output, of course reading -larger output is going to be harder which is where prove(1) comes -in. prove is a harness program that reads test output and produces -reports based on it: - - $ prove t/pass.t - t/pass....ok - All tests successful. - Files=1, Tests=1, 0 wallclock secs ( 0.03 cusr + 0.02 csys = 0.05 CPU) - -To run all the tests in the F directory recursively use C. This can be put in a F under a I target, for -example: - - test: Test.php - prove -r t - -For reference the example test file above looks like this, the shebang -on the first line is needed so that prove(1) and other test harness -programs know they're dealing with a PHP file. - - #!/usr/bin/env php - - -=head1 SEE ALSO - -L - The TAP protocol - -=head1 AUTHOR - -Evar ArnfjErE Bjarmason and Andy Armstrong - -=head1 LICENSING - -The author or authors of this code dedicate any and all copyright -interest in this code to the public domain. We make this dedication -for the benefit of the public at large and to the detriment of our -heirs and successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights this -code under copyright law. - -=cut - -*/ diff --git a/t/inc/Database.t b/t/inc/Database.t deleted file mode 100644 index 4367fcd1..00000000 --- a/t/inc/Database.t +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env php -addQuotes( NULL ), '==', - 'NULL', 'Add quotes to NULL' ); - -cmp_ok( $db->addQuotes( 1234 ), '==', - "'1234'", 'Add quotes to int' ); - -cmp_ok( $db->addQuotes( 1234.5678 ), '==', - "'1234.5678'", 'Add quotes to float' ); - -cmp_ok( $db->addQuotes( 'string' ), '==', - "'string'", 'Add quotes to string' ); - -cmp_ok( $db->addQuotes( "string's cause trouble" ), '==', - "'string\'s cause trouble'", 'Add quotes to quoted string' ); - -$sql = $db->fillPrepared( - 'SELECT * FROM interwiki', array() ); -cmp_ok( $sql, '==', - 'SELECT * FROM interwiki', 'FillPrepared empty' ); - -$sql = $db->fillPrepared( - 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?', - array( 4, "Snicker's_paradox" ) ); -cmp_ok( $sql, '==', - "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'", 'FillPrepared question' ); - -$sql = $db->fillPrepared( - 'SELECT user_id FROM ! WHERE user_name=?', - array( '"user"', "Slash's Dot" ) ); -cmp_ok( $sql, '==', - "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'", 'FillPrepared quoted' ); - -$sql = $db->fillPrepared( - "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'", - array( '"user"', "Slash's Dot" ) ); -cmp_ok( $sql, '==', - "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'", 'FillPrepared raw' ); diff --git a/t/inc/Global.t b/t/inc/Global.t deleted file mode 100644 index 7b2994e6..00000000 --- a/t/inc/Global.t +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env php -', $start, 'Time (compare)' ); - -$arr = wfArrayToCGI( - array( 'baz' => 'AT&T', 'ignore' => '' ), - array( 'foo' => 'bar', 'baz' => 'overridden value' ) ); -is( $arr, "baz=AT%26T&foo=bar", 'Array to CGI' ); - -$mime = mimeTypeMatch( 'text/html', array( - 'application/xhtml+xml' => 1.0, - 'text/html' => 0.7, - 'text/plain' => 0.3 -) ); -is( $mime, 'text/html', 'Mime (1)' ); - -$mime = mimeTypeMatch( 'text/html', array( - 'image/*' => 1.0, - 'text/*' => 0.5 -) ); -is( $mime, 'text/*', 'Mime (2)' ); - -$mime = mimeTypeMatch( 'text/html', array( '*/*' => 1.0 ) ); -is( $mime, '*/*', 'Mime (3)' ); - -$mime = mimeTypeMatch( 'text/html', array( - 'image/png' => 1.0, - 'image/svg+xml' => 0.5 -) ); -is( $mime, null, 'Mime (4)' ); - -$mime = wfNegotiateType( - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.7, - 'text/plain' => 0.5, - 'text/*' => 0.2 ), - array( 'text/html' => 1.0 ) ); -is( $mime, 'text/html', 'Negotiate Mime (1)' ); - -$mime = wfNegotiateType( - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.7, - 'text/plain' => 0.5, - 'text/*' => 0.2 ), - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.5 ) ); -is( $mime, 'application/xhtml+xml', 'Negotiate Mime (2)' ); - -$mime = wfNegotiateType( - array( 'text/html' => 1.0, - 'text/plain' => 0.5, - 'text/*' => 0.5, - 'application/xhtml+xml' => 0.2 ), - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.5 ) ); -is( $mime, 'text/html', 'Negotiate Mime (3)' ); - -$mime = wfNegotiateType( - array( 'text/*' => 1.0, - 'image/*' => 0.7, - '*/*' => 0.3 ), - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.5 ) ); -is( $mime, 'text/html', 'Negotiate Mime (4)' ); - -$mime = wfNegotiateType( - array( 'text/*' => 1.0 ), - array( 'application/xhtml+xml' => 1.0 ) ); -is( $mime, null, 'Negotiate Mime (5)' ); - -$t = gmmktime( 12, 34, 56, 1, 15, 2001 ); -is( wfTimestamp( TS_MW, $t ), '20010115123456', 'TS_UNIX to TS_MW' ); -is( wfTimestamp( TS_UNIX, $t ), 979562096, 'TS_UNIX to TS_UNIX' ); -is( wfTimestamp( TS_DB, $t ), '2001-01-15 12:34:56', 'TS_UNIX to TS_DB' ); -$t = '20010115123456'; -is( wfTimestamp( TS_MW, $t ), '20010115123456', 'TS_MW to TS_MW' ); -is( wfTimestamp( TS_UNIX, $t ), 979562096, 'TS_MW to TS_UNIX' ); -is( wfTimestamp( TS_DB, $t ), '2001-01-15 12:34:56', 'TS_MW to TS_DB' ); -$t = '2001-01-15 12:34:56'; -is( wfTimestamp( TS_MW, $t ), '20010115123456', 'TS_DB to TS_MW' ); -is( wfTimestamp( TS_UNIX, $t ), 979562096, 'TS_DB to TS_UNIX' ); -is( wfTimestamp( TS_DB, $t ), '2001-01-15 12:34:56', 'TS_DB to TS_DB' ); - -$sets = array( - '' => '', - '/' => '', - '\\' => '', - '//' => '', - '\\\\' => '', - 'a' => 'a', - 'aaaa' => 'aaaa', - '/a' => 'a', - '\\a' => 'a', - '/aaaa' => 'aaaa', - '\\aaaa' => 'aaaa', - '/aaaa/' => 'aaaa', - '\\aaaa\\' => 'aaaa', - '\\aaaa\\' => 'aaaa', - '/mnt/upload3/wikipedia/en/thumb/8/8b/Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg' => '93px-Zork_Grand_Inquisitor_box_cover.jpg', - 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE' => 'VIEWER.EXE', - 'Östergötland_coat_of_arms.png' => 'Östergötland_coat_of_arms.png', -); -foreach( $sets as $from => $to ) { - is( $to, wfBaseName( $from ), - "wfBaseName('$from') => '$to'"); -} \ No newline at end of file diff --git a/t/inc/IP.t b/t/inc/IP.t deleted file mode 100644 index eb717252..00000000 --- a/t/inc/IP.t +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env php - 50, - 'height' => 50, - 'tests' => array( - 50 => 50, - 17 => 17, - 18 => 18 ) ), - array( - 'width' => 366, - 'height' => 300, - 'tests' => array( - 50 => 61, - 17 => 21, - 18 => 22 ) ), - array( - 'width' => 300, - 'height' => 366, - 'tests' => array( - 50 => 41, - 17 => 14, - 18 => 15 ) ), - array( - 'width' => 100, - 'height' => 400, - 'tests' => array( - 50 => 12, - 17 => 4, - 18 => 4 ) ) -); - -plan( 3 + 3 * count( $vals ) ); - -require_ok( 'includes/ProfilerStub.php' ); -require_ok( 'includes/GlobalFunctions.php' ); -require_ok( 'includes/ImageFunctions.php' ); - -foreach( $vals as $row ) { - extract( $row ); - foreach( $tests as $max => $expected ) { - $y = round( $expected * $height / $width ); - $result = wfFitBoxWidth( $width, $height, $max ); - $y2 = round( $result * $height / $width ); - is( $result, $expected, - "($width, $height, $max) wanted: {$expected}x{$y}, got: {$result}x{$y2}" ); - } -} - diff --git a/t/inc/Language.t b/t/inc/Language.t deleted file mode 100644 index 125d67c1..00000000 --- a/t/inc/Language.t +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env php -userAdjust( $date, '' ) ), - '==', - strval( $expected ), - "User adjust {$date} by {$offset} minutes should give {$expected}" - ); -} - -# Collection of parameters for Language_t_Offset. -# Format: date to be formatted, localTZoffset value, expected date -$userAdjust_tests = array( - array( 20061231235959, 0, 20061231235959 ), - array( 20061231235959, 5, 20070101000459 ), - array( 20061231235959, 15, 20070101001459 ), - array( 20061231235959, 60, 20070101005959 ), - array( 20061231235959, 90, 20070101012959 ), - array( 20061231235959, 120, 20070101015959 ), - array( 20061231235959, 540, 20070101085959 ), - array( 20061231235959, -5, 20061231235459 ), - array( 20061231235959, -30, 20061231232959 ), - array( 20061231235959, -60, 20061231225959 ), -); - -plan( count($userAdjust_tests) ); -define( 'MEDIAWIKI', 1 ); - -# Don't use require_ok as these files need global variables - -require 'includes/Defines.php'; -require 'includes/ProfilerStub.php'; - -require 'LocalSettings.php'; -require 'includes/DefaultSettings.php'; - -require 'includes/Setup.php'; - -# Create a language object -$wgContLang = $en = Language::factory( 'en' ); - -global $wgUser; -$wgUser = new User(); - -# Launch tests for language::userAdjust -foreach( $userAdjust_tests as $data ) { - test_userAdjust( $en, $data[0], $data[1], $data[2] ); -} - -/* vim: set filetype=php: */ diff --git a/t/inc/Licenses.t b/t/inc/Licenses.t deleted file mode 100644 index 81e7abe9..00000000 --- a/t/inc/Licenses.t +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env php -html; - -/* vim: set filetype=php: */ diff --git a/t/inc/LocalFile.t b/t/inc/LocalFile.t deleted file mode 100644 index 09df9e19..00000000 --- a/t/inc/LocalFile.t +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env php - 'test', - 'directory' => '/testdir', - 'url' => '/testurl', - 'hashLevels' => 2, - 'transformVia404' => false, -); - -plan( 35 ); - -$repo_hl0 = new LocalRepo( array( 'hashLevels' => 0 ) + $info ); -$repo_hl2 = new LocalRepo( array( 'hashLevels' => 2 ) + $info ); -$repo_lc = new LocalRepo( array( 'initialCapital' => false ) + $info ); - -$file_hl0 = $repo_hl0->newFile( 'test!' ); -$file_hl2 = $repo_hl2->newFile( 'test!' ); -$file_lc = $repo_lc->newFile( 'test!' ); - -is( $file_hl0->getHashPath(), '', 'Get hash path, hasLev 0' ); -is( $file_hl2->getHashPath(), 'a/a2/', 'Get hash path, hasLev 2' ); -is( $file_lc->getHashPath(), 'c/c4/', 'Get hash path, lc first' ); - -is( $file_hl0->getRel(), 'Test!', 'Get rel path, hasLev 0' ); -is( $file_hl2->getRel(), 'a/a2/Test!', 'Get rel path, hasLev 2' ); -is( $file_lc->getRel(), 'c/c4/test!', 'Get rel path, lc first' ); - -is( $file_hl0->getUrlRel(), 'Test%21', 'Get rel url, hasLev 0' ); -is( $file_hl2->getUrlRel(), 'a/a2/Test%21', 'Get rel url, hasLev 2' ); -is( $file_lc->getUrlRel(), 'c/c4/test%21', 'Get rel url, lc first' ); - -is( $file_hl0->getArchivePath(), '/testdir/archive', 'Get archive path, hasLev 0' ); -is( $file_hl2->getArchivePath(), '/testdir/archive/a/a2', 'Get archive path, hasLev 2' ); -is( $file_hl0->getArchivePath( '!' ), '/testdir/archive/!', 'Get archive path, hasLev 0' ); -is( $file_hl2->getArchivePath( '!' ), '/testdir/archive/a/a2/!', 'Get archive path, hasLev 2' ); - -is( $file_hl0->getThumbPath(), '/testdir/thumb/Test!', 'Get thumb path, hasLev 0' ); -is( $file_hl2->getThumbPath(), '/testdir/thumb/a/a2/Test!', 'Get thumb path, hasLev 2' ); -is( $file_hl0->getThumbPath( 'x' ), '/testdir/thumb/Test!/x', 'Get thumb path, hasLev 0' ); -is( $file_hl2->getThumbPath( 'x' ), '/testdir/thumb/a/a2/Test!/x', 'Get thumb path, hasLev 2' ); - -is( $file_hl0->getArchiveUrl(), '/testurl/archive', 'Get archive url, hasLev 0' ); -is( $file_hl2->getArchiveUrl(), '/testurl/archive/a/a2', 'Get archive url, hasLev 2' ); -is( $file_hl0->getArchiveUrl( '!' ), '/testurl/archive/%21', 'Get archive url, hasLev 0' ); -is( $file_hl2->getArchiveUrl( '!' ), '/testurl/archive/a/a2/%21', 'Get archive url, hasLev 2' ); - -is( $file_hl0->getThumbUrl(), '/testurl/thumb/Test%21', 'Get thumb url, hasLev 0' ); -is( $file_hl2->getThumbUrl(), '/testurl/thumb/a/a2/Test%21', 'Get thumb url, hasLev 2' ); -is( $file_hl0->getThumbUrl( 'x' ), '/testurl/thumb/Test%21/x', 'Get thumb url, hasLev 0' ); -is( $file_hl2->getThumbUrl( 'x' ), '/testurl/thumb/a/a2/Test%21/x', 'Get thumb url, hasLev 2' ); - -is( $file_hl0->getArchiveVirtualUrl(), 'mwrepo://test/public/archive', 'Get archive virtual url, hasLev 0' ); -is( $file_hl2->getArchiveVirtualUrl(), 'mwrepo://test/public/archive/a/a2', 'Get archive virtual url, hasLev 2' ); -is( $file_hl0->getArchiveVirtualUrl( '!' ), 'mwrepo://test/public/archive/%21', 'Get archive virtual url, hasLev 0' ); -is( $file_hl2->getArchiveVirtualUrl( '!' ), 'mwrepo://test/public/archive/a/a2/%21', 'Get archive virtual url, hasLev 2' ); - -is( $file_hl0->getThumbVirtualUrl(), 'mwrepo://test/public/thumb/Test%21', 'Get thumb virtual url, hasLev 0' ); -is( $file_hl2->getThumbVirtualUrl(), 'mwrepo://test/public/thumb/a/a2/Test%21', 'Get thumb virtual url, hasLev 2' ); -is( $file_hl0->getThumbVirtualUrl( '!' ), 'mwrepo://test/public/thumb/Test%21/%21', 'Get thumb virtual url, hasLev 0' ); -is( $file_hl2->getThumbVirtualUrl( '!' ), 'mwrepo://test/public/thumb/a/a2/Test%21/%21', 'Get thumb virtual url, hasLev 2' ); - -is( $file_hl0->getUrl(), '/testurl/Test%21', 'Get url, hasLev 0' ); -is( $file_hl2->getUrl(), '/testurl/a/a2/Test%21', 'Get url, hasLev 2' ); diff --git a/t/inc/Parser.t b/t/inc/Parser.t deleted file mode 100644 index 9df21d9a..00000000 --- a/t/inc/Parser.t +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env php -showProgress = false; -$tester->showFailure = false; -$tester->recorder = new ProveTestRecorder( $tester->term ); - -// Do not output the number of tests, if will be done automatically at the end - -$tester->runTestsFromFiles( $wgParserTestFiles ); - -/* vim: set filetype=php: */ diff --git a/t/inc/Revision.t b/t/inc/Revision.t deleted file mode 100644 index a6f2849b..00000000 --- a/t/inc/Revision.t +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env php -old_flags = ''; -$row->old_text = 'This is a bunch of revision text.'; -cmp_ok( Revision::getRevisionText( $row ), '==', - 'This is a bunch of revision text.', 'Get revision text' ); - -$row = new stdClass; -$row->old_flags = 'gzip'; -$row->old_text = gzdeflate( 'This is a bunch of revision text.' ); -cmp_ok( Revision::getRevisionText( $row ), '==', - 'This is a bunch of revision text.', 'Get revision text with gzip compression' ); - -$wgLegacyEncoding = 'iso-8859-1'; - -$row = new stdClass; -$row->old_flags = 'utf-8'; -$row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; -cmp_ok( Revision::getRevisionText( $row ), '==', - "Wiki est l'\xc3\xa9cole superieur !", 'Get revision text utf-8 native' ); - -$row = new stdClass; -$row->old_flags = ''; -$row->old_text = "Wiki est l'\xe9cole superieur !"; -cmp_ok( Revision::getRevisionText( $row ), '==', - "Wiki est l'\xc3\xa9cole superieur !", 'Get revision text utf-8 legacy' ); - -$row = new stdClass; -$row->old_flags = 'gzip,utf-8'; -$row->old_text = gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ); -cmp_ok( Revision::getRevisionText( $row ), '==', - "Wiki est l'\xc3\xa9cole superieur !", 'Get revision text utf-8 native and gzip' ); - -$row = new stdClass; -$row->old_flags = 'gzip'; -$row->old_text = gzdeflate( "Wiki est l'\xe9cole superieur !" ); -cmp_ok( Revision::getRevisionText( $row ), '==', - "Wiki est l'\xc3\xa9cole superieur !", 'Get revision text utf-8 native and gzip' ); - -$row = new stdClass; -$row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; -$row->old_flags = Revision::compressRevisionText( $row->old_text ); -like( $row->old_flags, '/utf-8/', "Flags should contain 'utf-8'" ); -unlike( $row->old_flags, '/gzip/', "Flags should not contain 'gzip'" ); -cmp_ok( $row->old_text, '==', - "Wiki est l'\xc3\xa9cole superieur !", "Direct check" ); -cmp_ok( Revision::getRevisionText( $row ), '==', - "Wiki est l'\xc3\xa9cole superieur !", "getRevisionText" ); - -$wgCompressRevisions = true; - -$row = new stdClass; -$row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; -$row->old_flags = Revision::compressRevisionText( $row->old_text ); -like( $row->old_flags, '/utf-8/', "Flags should contain 'utf-8'" ); -like( $row->old_flags, '/gzip/', "Flags should contain 'gzip'" ); -cmp_ok( gzinflate( $row->old_text ), '==', - "Wiki est l'\xc3\xa9cole superieur !", "Direct check" ); -cmp_ok( Revision::getRevisionText( $row ), '==', - "Wiki est l'\xc3\xa9cole superieur !", "getRevisionText" ); diff --git a/t/inc/Sanitizer.t b/t/inc/Sanitizer.t deleted file mode 100644 index ae2c9a23..00000000 --- a/t/inc/Sanitizer.t +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env php -Hello world

    ' ), - '==', - '
    Hello world
    ', - 'Self-closing closing div' -); - -/* vim: set filetype=php: */ diff --git a/t/inc/Search.t b/t/inc/Search.t deleted file mode 100644 index 2f06dcd9..00000000 --- a/t/inc/Search.t +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env php -run(); - -/* vim: set filetype=php: */ diff --git a/t/inc/Title.t b/t/inc/Title.t deleted file mode 100644 index 7373b9f2..00000000 --- a/t/inc/Title.t +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env php -|", $chr ) !== false || preg_match( "/[\\x00-\\x1f\\x7f]/", $chr ) ) { - unlike( $chr, "/[$titlechars]/", "chr($num) = $chr is not a valid titlechar" ); - } else { - like( $chr, "/[$titlechars]/", "chr($num) = $chr is a valid titlechar" ); - } -} - -/* vim: set filetype=php: */ diff --git a/t/inc/Xml.t b/t/inc/Xml.t deleted file mode 100644 index b7cef881..00000000 --- a/t/inc/Xml.t +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env php -', - 'Opening element with no attributes' -); - -cmp_ok( - Xml::element( 'element', null, '' ), - '==', - '', - 'Terminated empty element' -); - -cmp_ok( - Xml::element( 'element', null, 'hello you & you' ), - '==', - 'hello <there> you & you', - 'Element with no attributes and content that needs escaping' -); - -cmp_ok( - Xml::element( 'element', array( 'key' => 'value', '<>' => '<>' ), null ), - '==', - '="<>">', - 'Element attributes, keys are not escaped' -); - -# -# open/close element -# - -cmp_ok( - Xml::openElement( 'element', array( 'k' => 'v' ) ), - '==', - '', - 'openElement() shortcut' -); - -cmp_ok( Xml::closeElement( 'element' ), '==', '', 'closeElement() shortcut' ); - -/* vim: set filetype=php: */ diff --git a/t/maint/bom.t b/t/maint/bom.t deleted file mode 100644 index b5e6ae98..00000000 --- a/t/maint/bom.t +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env perl -# -# This test detect Byte Order Mark (BOM). The char is sometime included at the -# top of files by some text editors to mark them as being UTF-8 encoded. -# They are not stripped by php 5.x and appear at the beginning of our content, -# You want them removed! -# See: -# http://www.fileformat.info/info/unicode/char/feff/index.htm -# http://bugzilla.wikimedia.org/show_bug.cgi?id=9954 - -use strict; -use warnings; - -use Test::More; - -use File::Find; - -# Files for wich we want to check the BOM char ( 0xFE 0XFF ) -my $ext = qr/(?:php|inc)/x ; - -my $bomchar = qr/\xef\xbb\xbf/ ; - -my @files; - -find( sub{ push @files, $File::Find::name if -f && /\.$ext$/ }, '.' ); - -# Register our files with the test system -plan tests => scalar @files ; - -for my $file (@files) { - open my $fh, "<", $file or die "Couln't open $file: $!"; - my $line = <$fh>; - if( $line =~ /$bomchar/ ) { - fail "$file has a Byte Order Mark at line $."; - } else { - pass "$file has no Byte Order Mark!"; - } -} diff --git a/t/maint/eol-style.t b/t/maint/eol-style.t deleted file mode 100644 index 2e281dc4..00000000 --- a/t/maint/eol-style.t +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env perl -# -# Based on php-tag.t -# -use strict; -use warnings; - -use Test::More; -use File::Find; -use IPC::Open3; -use File::Spec; -use Symbol qw(gensym); - -my $ext = qr/(?: php | inc | txt | sql | t)/x; -my @files; - -find( sub { push @files, $File::Find::name if -f && /\. $ext $/x }, '.' ); - -plan tests => scalar @files ; - -for my $file (@files) { - open NULL, '+>', File::Spec->devnull and \*NULL or die; - my $pid = open3('<&NULL', \*P, '>&NULL', qw'svn propget svn:eol-style', $file); - my $res = do { local $/;

    . "" }; - chomp $res; - waitpid $pid, 0; - - if ( $? != 0 ) { - pass "svn propget failed, $file probably not under version control"; - } elsif ( $res eq 'native' ) { - pass "$file svn:eol-style is 'native'"; - } else { - fail "$file svn:eol-style is '$res', should be 'native'"; - } -} diff --git a/t/maint/php-lint.t b/t/maint/php-lint.t deleted file mode 100644 index 6687a089..00000000 --- a/t/maint/php-lint.t +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env perl -# -# Based on php-tag.t and eol-style -# -use strict; -use warnings; - -use Test::More; -use File::Find; -use IPC::Open3; -use File::Spec; -use Symbol qw(gensym); - -my $ext = qr/(?: php | inc )/x; -my @files; - -find( sub { push @files, $File::Find::name if -f && /\. $ext $/x }, '.' ); - -plan tests => scalar @files ; - -for my $file (@files) { - open NULL, '+>', File::Spec->devnull and \*NULL or die; - my $pid = open3('<&NULL', \*P, '>&NULL', qw'php -l', $file); - my $res = do { local $/;

    . "" }; - chomp $res; - waitpid $pid, 0; - - if ( $? == 0 ) { - pass($file); - } else { - fail("$file does not pass php -l. Error was: $res"); - } -} diff --git a/t/maint/php-tag.t b/t/maint/php-tag.t deleted file mode 100644 index 5093ca7f..00000000 --- a/t/maint/php-tag.t +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use Test::More;; - -use File::Find; -use File::Slurp qw< slurp >; - -my $ext = qr/(?: php | inc )/x; - -my @files; -find( sub { push @files, $File::Find::name if -f && /\. $ext $/x }, '.' ); - -plan tests => scalar @files; - -for my $file (@files) { - my $cont = slurp $file; - if ( $cont =~ m<<\?php .* \?>>xs ) { - pass "$file has "; - } elsif ( $cont =~ m<<\? .* \?>>xs ) { - fail "$file does not use "; - } else { - pass "$file has neither nor , check it"; - } -} - - - diff --git a/t/maint/unix-newlines.t b/t/maint/unix-newlines.t deleted file mode 100644 index c47dd17c..00000000 --- a/t/maint/unix-newlines.t +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -use Test::More;; - -use File::Find; -use Socket '$CRLF'; - -my $ext = qr/(?: t | pm | sql | js | php | inc | xml )/x; - -my @files; -find( sub { push @files, $File::Find::name if -f && /\. $ext $/x }, '.' ); - -plan 'no_plan'; - -for my $file (@files) { - open my $fh, "<", $file or die "Can't open $file: $!"; - binmode $fh; - - my $ok = 1; - while (<$fh>) { - if (/$CRLF/) { - fail "$file has \\r\\n on line $."; - $ok = 0; - } - } - - pass "$file has only unix newlines" if $ok; -} - - - diff --git a/tests/.htaccess b/tests/.htaccess deleted file mode 100644 index 3a428827..00000000 --- a/tests/.htaccess +++ /dev/null @@ -1 +0,0 @@ -Deny from all diff --git a/tests/.svnignore b/tests/.svnignore deleted file mode 100644 index 20cb61e9..00000000 --- a/tests/.svnignore +++ /dev/null @@ -1,6 +0,0 @@ -LocalTestSettings.php -*~ -bin -.classpath -.project -project.index diff --git a/tests/ArticleTest.php b/tests/ArticleTest.php deleted file mode 100644 index a65e1ee3..00000000 --- a/tests/ArticleTest.php +++ /dev/null @@ -1,110 +0,0 @@ - false, - 'wgCompressRevisions' => false, - 'wgInputEncoding' => 'utf-8', - 'wgOutputEncoding' => 'utf-8' ); - foreach( $globalSet as $var => $data ) { - $this->saveGlobals[$var] = $GLOBALS[$var]; - $GLOBALS[$var] = $data; - } - } - - function tearDown() { - foreach( $this->saveGlobals as $var => $data ) { - $GLOBALS[$var] = $data; - } - } - - function testGetRevisionText() { - $row = new stdClass; - $row->old_flags = ''; - $row->old_text = 'This is a bunch of revision text.'; - $this->assertEquals( - 'This is a bunch of revision text.', - Revision::getRevisionText( $row ) ); - } - - function testGetRevisionTextGzip() { - $row = new stdClass; - $row->old_flags = 'gzip'; - $row->old_text = gzdeflate( 'This is a bunch of revision text.' ); - $this->assertEquals( - 'This is a bunch of revision text.', - Revision::getRevisionText( $row ) ); - } - - function testGetRevisionTextUtf8Native() { - $row = new stdClass; - $row->old_flags = 'utf-8'; - $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; - $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; - $this->assertEquals( - "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ) ); - } - - function testGetRevisionTextUtf8Legacy() { - $row = new stdClass; - $row->old_flags = ''; - $row->old_text = "Wiki est l'\xe9cole superieur !"; - $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; - $this->assertEquals( - "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ) ); - } - - function testGetRevisionTextUtf8NativeGzip() { - $row = new stdClass; - $row->old_flags = 'gzip,utf-8'; - $row->old_text = gzdeflate( "Wiki est l'\xc3\xa9cole superieur !" ); - $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; - $this->assertEquals( - "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ) ); - } - - function testGetRevisionTextUtf8LegacyGzip() { - $row = new stdClass; - $row->old_flags = 'gzip'; - $row->old_text = gzdeflate( "Wiki est l'\xe9cole superieur !" ); - $GLOBALS['wgLegacyEncoding'] = 'iso-8859-1'; - $this->assertEquals( - "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ) ); - } - - function testCompressRevisionTextUtf8() { - $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; - $row->old_flags = Revision::compressRevisionText( $row->old_text ); - $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), - "Flags should contain 'utf-8'" ); - $this->assertFalse( false !== strpos( $row->old_flags, 'gzip' ), - "Flags should not contain 'gzip'" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - $row->old_text, "Direct check" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ), "getRevisionText" ); - } - - function testCompressRevisionTextUtf8Gzip() { - $GLOBALS['wgCompressRevisions'] = true; - $row->old_text = "Wiki est l'\xc3\xa9cole superieur !"; - $row->old_flags = Revision::compressRevisionText( $row->old_text ); - $this->assertTrue( false !== strpos( $row->old_flags, 'utf-8' ), - "Flags should contain 'utf-8'" ); - $this->assertTrue( false !== strpos( $row->old_flags, 'gzip' ), - "Flags should contain 'gzip'" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - gzinflate( $row->old_text ), "Direct check" ); - $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !", - Revision::getRevisionText( $row ), "getRevisionText" ); - } -} - - diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php deleted file mode 100644 index db46ad60..00000000 --- a/tests/DatabaseTest.php +++ /dev/null @@ -1,80 +0,0 @@ -db = wfGetDB( DB_SLAVE ); - } - - function testAddQuotesNull() { - $this->assertEquals( - 'NULL', - $this->db->addQuotes( NULL ) ); - } - - function testAddQuotesInt() { - # returning just "1234" should be ok too, though... - # maybe - $this->assertEquals( - "'1234'", - $this->db->addQuotes( 1234 ) ); - } - - function testAddQuotesFloat() { - # returning just "1234.5678" would be ok too, though - $this->assertEquals( - "'1234.5678'", - $this->db->addQuotes( 1234.5678 ) ); - } - - function testAddQuotesString() { - $this->assertEquals( - "'string'", - $this->db->addQuotes( 'string' ) ); - } - - function testAddQuotesStringQuote() { - $this->assertEquals( - "'string\'s cause trouble'", - $this->db->addQuotes( "string's cause trouble" ) ); - } - - function testFillPreparedEmpty() { - $sql = $this->db->fillPrepared( - 'SELECT * FROM interwiki', array() ); - $this->assertEquals( - "SELECT * FROM interwiki", - $sql); - } - - function testFillPreparedQuestion() { - $sql = $this->db->fillPrepared( - 'SELECT * FROM cur WHERE cur_namespace=? AND cur_title=?', - array( 4, "Snicker's_paradox" ) ); - $this->assertEquals( - "SELECT * FROM cur WHERE cur_namespace='4' AND cur_title='Snicker\'s_paradox'", - $sql); - } - - function testFillPreparedBang() { - $sql = $this->db->fillPrepared( - 'SELECT user_id FROM ! WHERE user_name=?', - array( '"user"', "Slash's Dot" ) ); - $this->assertEquals( - "SELECT user_id FROM \"user\" WHERE user_name='Slash\'s Dot'", - $sql); - } - - function testFillPreparedRaw() { - $sql = $this->db->fillPrepared( - "SELECT * FROM cur WHERE cur_title='This_\\&_that,_WTF\\?\\!'", - array( '"user"', "Slash's Dot" ) ); - $this->assertEquals( - "SELECT * FROM cur WHERE cur_title='This_&_that,_WTF?!'", - $sql); - } - -} - - diff --git a/tests/GlobalTest.php b/tests/GlobalTest.php deleted file mode 100644 index ec694241..00000000 --- a/tests/GlobalTest.php +++ /dev/null @@ -1,212 +0,0 @@ -originals['wgReadOnlyFile'] = $wgReadOnlyFile; - $wgReadOnlyFile = tempnam(wfTempDir(), "mwtest_readonly"); - unlink( $wgReadOnlyFile ); - } - - function tearDown() { - global $wgReadOnlyFile; - if( file_exists( $wgReadOnlyFile ) ) { - unlink( $wgReadOnlyFile ); - } - $wgReadOnlyFile = $this->originals['wgReadOnlyFile']; - } - - function testRandom() { - # This could hypothetically fail, but it shouldn't ;) - $this->assertFalse( - wfRandom() == wfRandom() ); - } - - function testUrlencode() { - $this->assertEquals( - "%E7%89%B9%E5%88%A5:Contributions/Foobar", - wfUrlencode( "\xE7\x89\xB9\xE5\x88\xA5:Contributions/Foobar" ) ); - } - - function testReadOnlyEmpty() { - global $wgReadOnly; - $wgReadOnly = null; - - $this->assertFalse( wfReadOnly() ); - $this->assertFalse( wfReadOnly() ); - } - - function testReadOnlySet() { - global $wgReadOnly, $wgReadOnlyFile; - - $f = fopen( $wgReadOnlyFile, "wt" ); - fwrite( $f, 'Message' ); - fclose( $f ); - $wgReadOnly = null; - - $this->assertTrue( wfReadOnly() ); - $this->assertTrue( wfReadOnly() ); - - unlink( $wgReadOnlyFile ); - $wgReadOnly = null; - - $this->assertFalse( wfReadOnly() ); - $this->assertFalse( wfReadOnly() ); - } - - function testQuotedPrintable() { - $this->assertEquals( - "=?UTF-8?Q?=C4=88u=20legebla=3F?=", - wfQuotedPrintable( "\xc4\x88u legebla?", "UTF-8" ) ); - } - - function testTime() { - $start = wfTime(); - $this->assertType( 'float', $start ); - $end = wfTime(); - $this->assertTrue( $end > $start, "Time is running backwards!" ); - } - - function testArrayToCGI() { - $this->assertEquals( - "baz=AT%26T&foo=bar", - wfArrayToCGI( - array( 'baz' => 'AT&T', 'ignore' => '' ), - array( 'foo' => 'bar', 'baz' => 'overridden value' ) ) ); - } - - function testMimeTypeMatch() { - $this->assertEquals( - 'text/html', - mimeTypeMatch( 'text/html', - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.7, - 'text/plain' => 0.3 ) ) ); - $this->assertEquals( - 'text/*', - mimeTypeMatch( 'text/html', - array( 'image/*' => 1.0, - 'text/*' => 0.5 ) ) ); - $this->assertEquals( - '*/*', - mimeTypeMatch( 'text/html', - array( '*/*' => 1.0 ) ) ); - $this->assertNull( - mimeTypeMatch( 'text/html', - array( 'image/png' => 1.0, - 'image/svg+xml' => 0.5 ) ) ); - } - - function testNegotiateType() { - $this->assertEquals( - 'text/html', - wfNegotiateType( - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.7, - 'text/plain' => 0.5, - 'text/*' => 0.2 ), - array( 'text/html' => 1.0 ) ) ); - $this->assertEquals( - 'application/xhtml+xml', - wfNegotiateType( - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.7, - 'text/plain' => 0.5, - 'text/*' => 0.2 ), - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.5 ) ) ); - $this->assertEquals( - 'text/html', - wfNegotiateType( - array( 'text/html' => 1.0, - 'text/plain' => 0.5, - 'text/*' => 0.5, - 'application/xhtml+xml' => 0.2 ), - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.5 ) ) ); - $this->assertEquals( - 'text/html', - wfNegotiateType( - array( 'text/*' => 1.0, - 'image/*' => 0.7, - '*/*' => 0.3 ), - array( 'application/xhtml+xml' => 1.0, - 'text/html' => 0.5 ) ) ); - $this->assertNull( - wfNegotiateType( - array( 'text/*' => 1.0 ), - array( 'application/xhtml+xml' => 1.0 ) ) ); - } - - function testTimestamp() { - $t = gmmktime( 12, 34, 56, 1, 15, 2001 ); - $this->assertEquals( - '20010115123456', - wfTimestamp( TS_MW, $t ), - 'TS_UNIX to TS_MW' ); - $this->assertEquals( - 979562096, - wfTimestamp( TS_UNIX, $t ), - 'TS_UNIX to TS_UNIX' ); - $this->assertEquals( - '2001-01-15 12:34:56', - wfTimestamp( TS_DB, $t ), - 'TS_UNIX to TS_DB' ); - - $this->assertEquals( - '20010115123456', - wfTimestamp( TS_MW, '20010115123456' ), - 'TS_MW to TS_MW' ); - $this->assertEquals( - 979562096, - wfTimestamp( TS_UNIX, '20010115123456' ), - 'TS_MW to TS_UNIX' ); - $this->assertEquals( - '2001-01-15 12:34:56', - wfTimestamp( TS_DB, '20010115123456' ), - 'TS_MW to TS_DB' ); - - $this->assertEquals( - '20010115123456', - wfTimestamp( TS_MW, '2001-01-15 12:34:56' ), - 'TS_DB to TS_MW' ); - $this->assertEquals( - 979562096, - wfTimestamp( TS_UNIX, '2001-01-15 12:34:56' ), - 'TS_DB to TS_UNIX' ); - $this->assertEquals( - '2001-01-15 12:34:56', - wfTimestamp( TS_DB, '2001-01-15 12:34:56' ), - 'TS_DB to TS_DB' ); - } - - function testBasename() { - $sets = array( - '' => '', - '/' => '', - '\\' => '', - '//' => '', - '\\\\' => '', - 'a' => 'a', - 'aaaa' => 'aaaa', - '/a' => 'a', - '\\a' => 'a', - '/aaaa' => 'aaaa', - '\\aaaa' => 'aaaa', - '/aaaa/' => 'aaaa', - '\\aaaa\\' => 'aaaa', - '\\aaaa\\' => 'aaaa', - '/mnt/upload3/wikipedia/en/thumb/8/8b/Zork_Grand_Inquisitor_box_cover.jpg/93px-Zork_Grand_Inquisitor_box_cover.jpg' => '93px-Zork_Grand_Inquisitor_box_cover.jpg', - 'C:\\Progra~1\\Wikime~1\\Wikipe~1\\VIEWER.EXE' => 'VIEWER.EXE', - 'Östergötland_coat_of_arms.png' => 'Östergötland_coat_of_arms.png', - ); - foreach( $sets as $from => $to ) { - $this->assertEquals( $to, wfBaseName( $from ), - "wfBaseName('$from') => '$to'"); - } - } - - /* TODO: many more! */ -} - - diff --git a/tests/ImageFunctionsTest.php b/tests/ImageFunctionsTest.php deleted file mode 100644 index 9794a2a2..00000000 --- a/tests/ImageFunctionsTest.php +++ /dev/null @@ -1,48 +0,0 @@ - 50, - 'height' => 50, - 'tests' => array( - 50 => 50, - 17 => 17, - 18 => 18 ) ), - array( - 'width' => 366, - 'height' => 300, - 'tests' => array( - 50 => 61, - 17 => 21, - 18 => 22 ) ), - array( - 'width' => 300, - 'height' => 366, - 'tests' => array( - 50 => 41, - 17 => 14, - 18 => 15 ) ), - array( - 'width' => 100, - 'height' => 400, - 'tests' => array( - 50 => 12, - 17 => 4, - 18 => 4 ) ) ); - foreach( $vals as $row ) { - extract( $row ); - foreach( $tests as $max => $expected ) { - $y = round( $expected * $height / $width ); - $result = wfFitBoxWidth( $width, $height, $max ); - $y2 = round( $result * $height / $width ); - $this->assertEquals( $expected, - $result, - "($width, $height, $max) wanted: {$expected}x$y, got: {$result}x$y2" ); - } - } - } -} - - diff --git a/tests/LocalFileTest.php b/tests/LocalFileTest.php deleted file mode 100644 index 335b8bbe..00000000 --- a/tests/LocalFileTest.php +++ /dev/null @@ -1,90 +0,0 @@ - 'test', - 'directory' => '/testdir', - 'url' => '/testurl', - 'hashLevels' => 2, - 'transformVia404' => false, - ); - $this->repo_hl0 = new LocalRepo( array( 'hashLevels' => 0 ) + $info ); - $this->repo_hl2 = new LocalRepo( array( 'hashLevels' => 2 ) + $info ); - $this->repo_lc = new LocalRepo( array( 'initialCapital' => false ) + $info ); - $this->file_hl0 = $this->repo_hl0->newFile( 'test!' ); - $this->file_hl2 = $this->repo_hl2->newFile( 'test!' ); - $this->file_lc = $this->repo_lc->newFile( 'test!' ); - } - - function testGetHashPath() { - $this->assertEquals( '', $this->file_hl0->getHashPath() ); - $this->assertEquals( 'a/a2/', $this->file_hl2->getHashPath() ); - $this->assertEquals( 'c/c4/', $this->file_lc->getHashPath() ); - } - - function testGetRel() { - $this->assertEquals( 'Test!', $this->file_hl0->getRel() ); - $this->assertEquals( 'a/a2/Test!', $this->file_hl2->getRel() ); - $this->assertEquals( 'c/c4/test!', $this->file_lc->getRel() ); - } - - function testGetUrlRel() { - $this->assertEquals( 'Test%21', $this->file_hl0->getUrlRel() ); - $this->assertEquals( 'a/a2/Test%21', $this->file_hl2->getUrlRel() ); - $this->assertEquals( 'c/c4/test%21', $this->file_lc->getUrlRel() ); - } - - function testGetArchivePath() { - $this->assertEquals( '/testdir/archive', $this->file_hl0->getArchivePath() ); - $this->assertEquals( '/testdir/archive/a/a2', $this->file_hl2->getArchivePath() ); - $this->assertEquals( '/testdir/archive/!', $this->file_hl0->getArchivePath( '!' ) ); - $this->assertEquals( '/testdir/archive/a/a2/!', $this->file_hl2->getArchivePath( '!' ) ); - } - - function testGetThumbPath() { - $this->assertEquals( '/testdir/thumb/Test!', $this->file_hl0->getThumbPath() ); - $this->assertEquals( '/testdir/thumb/a/a2/Test!', $this->file_hl2->getThumbPath() ); - $this->assertEquals( '/testdir/thumb/Test!/x', $this->file_hl0->getThumbPath( 'x' ) ); - $this->assertEquals( '/testdir/thumb/a/a2/Test!/x', $this->file_hl2->getThumbPath( 'x' ) ); - } - - function testGetArchiveUrl() { - $this->assertEquals( '/testurl/archive', $this->file_hl0->getArchiveUrl() ); - $this->assertEquals( '/testurl/archive/a/a2', $this->file_hl2->getArchiveUrl() ); - $this->assertEquals( '/testurl/archive/%21', $this->file_hl0->getArchiveUrl( '!' ) ); - $this->assertEquals( '/testurl/archive/a/a2/%21', $this->file_hl2->getArchiveUrl( '!' ) ); - } - - function testGetThumbUrl() { - $this->assertEquals( '/testurl/thumb/Test%21', $this->file_hl0->getThumbUrl() ); - $this->assertEquals( '/testurl/thumb/a/a2/Test%21', $this->file_hl2->getThumbUrl() ); - $this->assertEquals( '/testurl/thumb/Test%21/x', $this->file_hl0->getThumbUrl( 'x' ) ); - $this->assertEquals( '/testurl/thumb/a/a2/Test%21/x', $this->file_hl2->getThumbUrl( 'x' ) ); - } - - function testGetArchiveVirtualUrl() { - $this->assertEquals( 'mwrepo://test/public/archive', $this->file_hl0->getArchiveVirtualUrl() ); - $this->assertEquals( 'mwrepo://test/public/archive/a/a2', $this->file_hl2->getArchiveVirtualUrl() ); - $this->assertEquals( 'mwrepo://test/public/archive/%21', $this->file_hl0->getArchiveVirtualUrl( '!' ) ); - $this->assertEquals( 'mwrepo://test/public/archive/a/a2/%21', $this->file_hl2->getArchiveVirtualUrl( '!' ) ); - } - - function testGetThumbVirtualUrl() { - $this->assertEquals( 'mwrepo://test/public/thumb/Test%21', $this->file_hl0->getThumbVirtualUrl() ); - $this->assertEquals( 'mwrepo://test/public/thumb/a/a2/Test%21', $this->file_hl2->getThumbVirtualUrl() ); - $this->assertEquals( 'mwrepo://test/public/thumb/Test%21/%21', $this->file_hl0->getThumbVirtualUrl( '!' ) ); - $this->assertEquals( 'mwrepo://test/public/thumb/a/a2/Test%21/%21', $this->file_hl2->getThumbVirtualUrl( '!' ) ); - } - - function testGetUrl() { - $this->assertEquals( '/testurl/Test%21', $this->file_hl0->getUrl() ); - $this->assertEquals( '/testurl/a/a2/Test%21', $this->file_hl2->getUrl() ); - } -} - - diff --git a/tests/Makefile b/tests/Makefile deleted file mode 100644 index 25ccda35..00000000 --- a/tests/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -.PHONY: help test -all test: - php run-test.php ArticleTest.php - php run-test.php GlobalTest.php - php run-test.php DatabaseTest.php - php run-test.php ImageFunctionsTest.php - php run-test.php SearchMySQL4Test.php -install: - cvs -z9 -d:pserver:cvsread:@cvs.php.net:/repository/ co -P pear/PHPUnit - mv pear/PHPUnit . - rm -rf pear -clean: - rm -rf PHPUnit pear -help: - # Options: - # test (default) Run the unit tests - # install Install PHPUnit from CVS - # clean Remove local PHPUnit install - # help You're looking at it! diff --git a/tests/MediaWiki_TestCase.php b/tests/MediaWiki_TestCase.php deleted file mode 100644 index 387fe011..00000000 --- a/tests/MediaWiki_TestCase.php +++ /dev/null @@ -1,51 +0,0 @@ -isOpen() ) { - if (!(strcmp($db->getServerVersion(), '4.1') < 0 and stristr($db->getSoftwareLink(), 'MySQL'))) { - # Database that supports CREATE TABLE ... LIKE - foreach ($tables as $tbl) { - $newTableName = $db->tableName( $tbl ); - #$tableName = $this->oldTableNames[$tbl]; - $tableName = $tbl; - $db->query("CREATE TEMPORARY TABLE $newTableName (LIKE $tableName)"); - } - } else { - # 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.... - foreach ($tables as $tbl) { - $res = $db->query("SHOW CREATE TABLE $tbl"); - $row = $db->fetchRow($res); - $create = $row[1]; - $create_tmp = preg_replace('/CREATE TABLE `(.*?)`/', 'CREATE TEMPORARY TABLE `' - . $wgDBprefix . '\\1`', $create); - if ($create === $create_tmp) { - # Couldn't do replacement - wfDie( "could not create temporary table $tbl" ); - } - $db->query($create_tmp); - } - - } - return $db; - } else { - // Something amiss - return null; - } - } -} - diff --git a/tests/README b/tests/README deleted file mode 100644 index 3bbf9704..00000000 --- a/tests/README +++ /dev/null @@ -1,9 +0,0 @@ -Some quickie unit tests done with the PHPUnit testing framework. To run the -test suite, run 'make test' in this dir or 'php RunTests.php' - -PHPUnit is no longer maintained by PEAR. To get the current version of -PHPUnit, first uninstall any old version of PHPUnit or PHPUnit2 from PEAR, -then install the current version from phpunit.de like this: - -# pear channel-discover pear.phpunit.de -# pear install phpunit/PHPUnit diff --git a/tests/SearchEngineTest.php b/tests/SearchEngineTest.php deleted file mode 100644 index 0ae14bdd..00000000 --- a/tests/SearchEngineTest.php +++ /dev/null @@ -1,136 +0,0 @@ -db->safeQuery( <<db->tableName( 'page' ) ); - $this->db->safeQuery( <<db->tableName( 'revision' ) ); - $this->db->safeQuery( <<db->tableName( 'text' ) ); - $this->db->safeQuery( <<db->tableName( 'searchindex' ) ); - } - - function fetchIds( &$results ) { - $matches = array(); - while( $row = $results->next() ) { - $matches[] = $row->getTitle()->getPrefixedText(); - } - $results->free(); - # Search is not guaranteed to return results in a certain order; - # sort them numerically so we will compare simply that we received - # the expected matches. - sort( $matches ); - return $matches; - } - - function testTextSearch() { - $this->assertFalse( is_null( $this->db ), "Can't find a database to test with." ); - if( !is_null( $this->db ) ) { - $this->assertEquals( - array( 'Smithee' ), - $this->fetchIds( $this->search->searchText( 'smithee' ) ), - "Plain search failed" ); - } - } - - function testTextPowerSearch() { - $this->assertFalse( is_null( $this->db ), "Can't find a database to test with." ); - if( !is_null( $this->db ) ) { - $this->search->setNamespaces( array( 0, 1, 4 ) ); - $this->assertEquals( - array( - 'Smithee', - 'Talk:Main Page', - ), - $this->fetchIds( $this->search->searchText( 'smithee' ) ), - "Power search failed" ); - } - } - - function testTitleSearch() { - $this->assertFalse( is_null( $this->db ), "Can't find a database to test with." ); - if( !is_null( $this->db ) ) { - $this->assertEquals( - array( - 'Alan Smithee', - 'Smithee', - ), - $this->fetchIds( $this->search->searchTitle( 'smithee' ) ), - "Title search failed" ); - } - } - - function testTextTitlePowerSearch() { - $this->assertFalse( is_null( $this->db ), "Can't find a database to test with." ); - if( !is_null( $this->db ) ) { - $this->search->setNamespaces( array( 0, 1, 4 ) ); - $this->assertEquals( - array( - 'Alan Smithee', - 'Smithee', - 'Talk:Smithee', - ), - $this->fetchIds( $this->search->searchTitle( 'smithee' ) ), - "Title power search failed" ); - } - } - -} - - - diff --git a/tests/SearchMySQL4Test.php b/tests/SearchMySQL4Test.php deleted file mode 100644 index 0f3a4c2c..00000000 --- a/tests/SearchMySQL4Test.php +++ /dev/null @@ -1,31 +0,0 @@ -db = $this->buildTestDatabase( - array( 'page', 'revision', 'text', 'searchindex' ) ); - if( $this->db ) { - $this->insertSearchData(); - } - $this->search = new SearchMySQL4( $this->db ); - } - - function tearDown() { - if( !is_null( $this->db ) ) { - $this->db->close(); - } - unset( $this->db ); - unset( $this->search ); - } - -} - - diff --git a/tests/run-test.php b/tests/run-test.php deleted file mode 100644 index 37ca8519..00000000 --- a/tests/run-test.php +++ /dev/null @@ -1,7 +0,0 @@ - - - DemoWiki - http://example.com/wiki/Main_Page - MediaWiki 1.5.0 - first-letter - - Media - Special - - Talk - User - User talk - DemoWiki - DemoWIki talk - Image - Image talk - MediaWiki - MediaWiki talk - Template - Template talk - Help - Help talk - Category - Category talk - - - - First page - 1 - - 1 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 1 - page 1, rev 1 - - - 2 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 2 - page 1, rev 2 - - - 4 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 4 - page 1, rev 4 - - - - Second page - 2 - - 3 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 2, rev 3 - page 2, rev 3 - - - - Third page - 3 - - 5 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 3, rev 5 - page 3, rev 5 - - - diff --git a/tests/test-prefetch-previous.xml b/tests/test-prefetch-previous.xml deleted file mode 100644 index 95eb82dd..00000000 --- a/tests/test-prefetch-previous.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - DemoWiki - http://example.com/wiki/Main_Page - MediaWiki 1.5.0 - first-letter - - Media - Special - - Talk - User - User talk - DemoWiki - DemoWIki talk - Image - Image talk - MediaWiki - MediaWiki talk - Template - Template talk - Help - Help talk - Category - Category talk - - - - First page - 1 - - 1 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 1 - page 1, rev 1 - - - 2 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 2 - page 1, rev 2 - - - - Second page - 2 - - 3 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 2, rev 3 - page 2, rev 3 - - - diff --git a/tests/test-prefetch-stub.xml b/tests/test-prefetch-stub.xml deleted file mode 100644 index 59d43d2f..00000000 --- a/tests/test-prefetch-stub.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - DemoWiki - http://example.com/wiki/Main_Page - MediaWiki 1.5.0 - first-letter - - Media - Special - - Talk - User - User talk - DemoWiki - DemoWIki talk - Image - Image talk - MediaWiki - MediaWiki talk - Template - Template talk - Help - Help talk - Category - Category talk - - - - First page - 1 - - 1 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 1 - - - - 2 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 2 - - - - 4 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 1, rev 4 - - - - - Second page - 2 - - 3 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 2, rev 3 - - - - - Third page - 3 - - 5 - 2001-01-15T12:00:00Z - 10.0.0.1 - page 3, rev 5 - - - - diff --git a/wiki.phtml b/wiki.phtml index 5f6b0199..435dff0e 100644 --- a/wiki.phtml +++ b/wiki.phtml @@ -1,4 +1,3 @@ \ No newline at end of file -- cgit v1.2.3-54-g00ecf